函数
导言
大家好,很高兴又和大家见面啦!经过前段时间的学习,我们已经对分支与循环语句有了一个基本的认知,咱们也通过了一些编码题加深了对这些知识点的理解与运用。今天开始,咱们将进入下一个内容的学习——函数。
一、函数是什么?
数学里的函数
在数学里函数的近代定义是给定一个数集A,假设其中的元素为x,对A中的元素x施加对应法则f,记作f(x),得到另一数集B,假设B中的元素为y,则y与x之间的等量关系可以用y=f(x)表示。 函数概念含有三个要素:定义域A、值域B和对应法则f。 这其中定义域的元素称为自变量,值域里的元素称为因变量。 在计算机的世界里同样也是如此,一个函数同样也是有自变量、因变量以及对应法则。
计算机中的函数
上面是我通过类比的方式,将数学中的函数类比到咱们的C语言中,今天我们来看一下在维基百科中对函数的定义:
1.在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。 2.一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
这里面的子程序也就是咱们说的函数,从上面的描述我们可以知道一下几点:
1.函数可以是一个语句,也可以是多个语句; 2.函数我们可以理解为就是咱们所说的功能,每个函数都有它自己的功能,也可以说是函数是为了实现这些功能才存在的。相比于其它的代码,它是相对独立的,这里我理解的是我需要使用它的时候,它才能起作用,我不需要使用它的时候,它也能存在于咱们的代码里这里我举个例子来证明一下:
大家可以看到,在这个代码中,我们编写了一个求两数之和的函数SUM,但是在主程序中,我们执行的是打印hello,并没有去使用这个函数,但是此时这个函数确实存在于我们的代码中,但是它并不会影响我们的代码正常运行,也不一定非得运行,相比于主函数来说,我们编写的SUM函数是相对独立的。
二、C语言中函数的分类
在C语言中函数分为两种:库函数、自定义函数。那什么是库函数,什么是自定义函数呢?咱们现在就来揭晓。
库函数
1.什么是库函数
库函数简单的理解就是C语言数据库里面的函数。
2.为什么会有库函数
对于这个问题,我是这样理解的:我们要知道的是函数的作用就是来使我们能够更加高效方便的写代码;我们在编写代码的过程中,所有的程序员都会反反复复的去运用一些功能,比如输入、输出……如果每一个程序员在使用这些功能之前都需要将这些功能重新编写一遍的话,这样就大大增加了工作量; 所以,方便程序员更加高效的编写代码,C语言就将这些函数给汇总并添加进了数据库中,这也就是所谓的库函数。换一种角度来看,库函数其实也是自定义函数,只不过这个自定义是C语言库的自定义。
3.怎么学习库函数呢?
这里给大家推荐几个途径可以学习库函数:(1)www.cplusplus.com;
从网站中可以看到C库中含有的头文件,通过这些头文件可以去学习相应头文件里面的库函数; (2)MSDN(Microsoft Developer Network) 这个软件咱们在前面编写猜数字游戏时有使用过,这里我就不展开叙述了,感兴趣的朋友可以再回顾一下相关内容。
4.库函数的分类
(1)IO函数(引用头文件<stdio.h>):
我们已经学习过的IO函数有:printf、scanf、getchar、putchar:从网站里我们能找到所有的IO函数,点击对应的函数,就能看到函数对应的信息,如字符输入函数——getchar:
通过这个网站我们更好的知道了getchar这个函数,这里我们可以简单概括一下:
• getchar读取的是标准输入中的字符,这里的标准输入就是我们前面提到的字符缓冲区; • 被读取字符的返回类型是int; • 读取EOF分为两种情况:
- 标准输入中的内容为文件结束标志,则返回EOF并设置标准输入的文件结束指示符;
- 发生读取错误时,也返回EOF但是设置的是错误指示符。
(2)字符串操作函数(引用头文件<string.h>):
我们已经学过的字符串操作函数有:strlen、strcmp,今天我们借助网站来学习一下strcpy——复制字符串:
通过网站内容,我们来对字符串拷贝函数做一个总结:
- 使用格式:strcpy(目的地地址,源头地址);
- strcpy可以将源头地址里的字符串包括结束字符也就是 一同拷贝到目标地址中;
- 目标地址的长度应该足够长,用来包含与源地址相同的字符串以及结束字符,并且不能与源地址重叠;下面我们举几个列子来进一步理解strcpy的用法:
#define _CRT_SECURE_NO_WARNINGS 1//防止使用strcpy时,VS程序报错
#include <stdio.h>//printf引用头文件;
#include <string.h>//strcpy引用头文件;
int main()
{
char a[] = "1234";
char b[10] = "abcdefgh";
strcpy(b, a);//将数组a的字符串复制到数组b的字符串中;
printf("%s", b);
return 0;
}
下面我们来看一下运行结果:
大家可以看到,此时数组a的字符串以及成功拷贝到了数组b中。现在有几个问题需要我们探讨一下: <1>字符串结束标志 是否有被拷贝到数组b中?<2>数组b中的其它字符去哪里了? 接下来我们通过调试中的监视窗口来一探究竟:
从监视窗口我们可以总结一下几点:
- 复制的过程类似于替换,将数组b中与数组a中对应下标的元素给一一替换,包括 ;
- 数组b中未被替换的元素任存在数组b中;
- 数组b中未被替换的元素之所以未被打印出来,是因为他们的前一位元素是字符串结束标志 ,打印函数读取到字符 时就停止了打印。
下面来举例子说明strcpy在结构体中的应用:
这里我们可以看到,我们定义的结构体struct book中的成员name是一个字符数组,我们在第一次使用时,数组中的元素是“5元如何用7天”这个字符串,如果我们需要修改这个里面的内容的话,就需要通过strcpy来将修改的字符串复制到该数组中,而结构体成员price是一个整型变量,我们在修改变量的值时只需要重新赋值就可以完成。 (注:结构体相关内容会在后面的篇章详细讲解,今天大家只做了解即可)
其它类型的库函数
对于strcpy这个函数我们就先学习到这里,接下来还有其它类型的库函数,我们简单了解一下: (3)字符操作函数、(4)内存操作函数、(5)时间/日期操作函数、(6)数学函数、(7)其它函数;对于这几个类型的库函数我们在前面的学习中有接触过时间函数(time—时间戳)和数学函数(sqrt—求平方根),这里就不展开探讨了,后面有机会,我们再来继续探讨这些不同的类型
自定义函数
1.自定义函数与库函数的异同
我对自定义函数的理解就是——自定义函数是库函数的一种补充;因为在写代码的过程中,并不是所有的问题都能用库函数解决,这时候就需要自定义函数来解决这些问题; 在早期没有库函数的时候,程序员需要使用打印、输入、输出等功能都需要自己先定义函数才行。这也就是为什么我觉得库函数也是自定义函数,而自定义函数是对库函数的补充。
相同点
自定义函数与库函数相同的地方就是它们都有函数名、返回值类型和函数参数;
不同点
它们的区别我们可以简单的理解为就是一个已经被定义好而且被收录在C语言库中,一个是由程序员根据实际情况进行设计且未被收入到C语言库中。 但是对于程序员来说,更重要的还是自定义函数。接下来我们开始探讨一下自定义函数:
2.函数的组成
自定义函数是由四个部分组成——函数返回类型、函数名、函数参数以及函数体,用代码展现的形式如下所示:
代码语言:javascript复制//函数的基本组成
ret_type fun_name(paral, *)//ret_type——返回类型;fun_name——函数名;paral——函数参数;
{
statement;//语句项也叫函数体——交代的是函数如何实现的
}
自定义函数的这四个部分具体有什么作用呢?下面我们通过实例来理解自定义函数;
3.自定义函数实例理解
为了更好的理解自定义函数,下面我们借助之前做过的题目找出两个整数的最大值来说明自定义函数的各个组成部分及其作用;
(1)写一个函数可以找出两个整数中的最大值:
代码语言:javascript复制//找出两个整数中的最大值
int MAX(int x, int y)//int——返回类型;MAX——函数名;x,y——函数参数;
{
if (x >= y)//通过if语句实现找出两数中的最大值;
return x;
else
return y;
}
int main()
{
int a, b;//定义参数;
scanf("%d%d", &a, &b);//通过scanf函数给两参数赋值;
int c = MAX(a, b);//将参数a,b传送给自定义函数MAX,并将MAX的返回值赋值给c;
printf("MAX=%dn", c);
return 0;
}
下面我们借助这个代码来分别介绍一下自定义函数的四个部分及其作用;
函数的返回类型
自定义函数的返回类型是为了确定函数的返回值的类型。这里我们定义的MAX函数是一个整型函数,所以我们在主函数中就定义了一个整型变量c来接收函数的返回值;
函数名
函数名是用来告诉别人我将要进行的操作。在这个例子中,我们是通过MAX这个函数名来告诉别人我需要找出两个整数的最大值;
函数参数
函数参数是我们在函数体中进行操作的对象。在这个例子中,我们需要在函数体中进行寻找的对象就是x、y这两个参数
函数体
函数体是我们用来交代函数是如何实现的。这个例子中,我们是通过将x和y进行比较大小,从而得到它们中的最大值。下面我们输入55、89来看一下运行结果:
可以看到,通过MAX这个函数我们很好的找出来这两个数中的最大值。
思维拓展
对于前面通过if……else语句来实现找出两数的最大值,这里我们也可以通过三目操作符来实现函数:
代码语言:javascript复制//找出两个整数中的最大值
int MAX(int x, int y)//int——返回类型;MAX——函数名;x,y——函数参数;
{
return (x >= y ? x : y);
}
int main()
{
int a, b;//定义参数;
scanf("%d%d", &a, &b);//通过scanf函数给两参数赋值;
int c = MAX(a, b);//将参数a,b传送给自定义函数MAX,并将MAX的返回值赋值给c;
printf("MAX=%dn", c);
return 0;
}
可以看到,在函数体中,我们直接通过三目操作符来找出这两个数的最大值,并将这个最大值返回给函数。这里我们也可以输入55、89来看一下运行结果:
从测试结果中可以看到,我们同样也能实现找出两数中的最大值。 我相信各位朋友通过这个例子能够更好的理解自定义函数了,对于自定义函数的返回类型、函数名和函数体我们都能理解其作用力,但是对于函数参数我们并没有在这个例子中去介绍。 在自定义函数中,我们在定义函数时的函数参数与主函数中调用函数时的函数参数有什么区别呢?接下来我们先来介绍一下;
三、函数的参数
参数的分类
1.实际参数(实参)
定义
真实传给函数的参数,叫实参。实参可以是:变量、常量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
2.形式参数(形参)
定义
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成后就自动销毁了,因此形式参数只在函数中有效。
对于参数定义的简单的理解就是我在调用函数时,传给函数的参数就叫做实参;在定义函数的时候,定义的参数就是形参。
看完这个定义我们还是感觉一头雾水,我们应该怎么来理解实参和形参以及它们之间的关系呢?接下来我们来看一道题;
(2)写一个函数可以交换两个整型变量的内容:
交换两个整型变量这个内容是不是有点熟悉啊,还记不记得我们在哪里有做过类似的题目呢?有朋友可能现在开始往回翻我前面写的博客了,我也就不给你们卖关子了,我们在分支与循环篇章的习题演练六中有介绍过将任意三个数按从大到小的顺序排列,感兴趣的朋友可以直接点击链接回顾一下这一题的解题思路,这里我就不再重复了。下面我们就尝试着通过函数的方式来完成这道题的代码编写:
代码语言:javascript复制//写一个函数可以交换两个整型变量的内容
void swap(int x, int y)//void——无返回类型;swap——函数名;x、y——参数;
{
int z = x;//函数体——交代函数如何实现
x = y;
y = z;
}
int main()
{
int a = 1;
int b = 2;
printf("a=%d b=%dn", a, b);
swap(a, b);
printf("a=%d b=%dn", a, b);
return 0;
}
在这个代码中,我们自定义了一个swap函数,可以看到,这个函数是一个无返回类型的函数,它的参数类型是整型,x和y就是这个自定义函数的参数,在函数体中我们通过辅助变量的方式来完成整型变量的交换。
接下来我们来根据实参的定义找一下这个代码中的实参和形参;所谓的实参就是我们在调用函数时传给函数的参数,在代码中我们传给swap函数的参数是a和b,也就是说a和b就是我们的实参;那同理,在定义函数时我们定义的参数就是形参,那么在这个代码中我们定义的参数x和y就是函数的形参。
现在大家应该理解什么是实参和形参了吧,那它们之间的关系又是什么呢?别着急,我们借助这个代码来探讨它们之间的关系;
这个测试结果并没有像我们想象的那样完成对a和b两个数的交换,为什么会这样呢?我们来调试一下代码查找一下原因;
这里我们可以看到,此时实参a/b的值成功的传输给了形参x/y,屏幕上也正常打印了此时a/b的值,接下来我们继续运行完成交换:
这里我们可以看到,此时x/y的值已经完成了交换,但是a/b还是原先的值。问题就出在这里!!!
那我们现在再来看一下a/b/x/y四个变量的空间地址,这里我们可以通过取地址操作符&来查看变量的地址,上图中&a、&b、&x、&y所对应的值就是这四个变量的空间地址;我们可以在图中清楚的看到,它们的空间地址并不相同。也就是说,它们四个是四个不同的变量,因此我们不能用x/y来代替a/b。那这样我们就可以像这样来理解实参和形参的关系:
当实参传给形参时,形参其实是实参的一份临时拷贝,对形参的修改是不会改变实参的。
这样也就是说如果我们要完成对a和b两数的交换,像这样去做是肯定行不通的,这是不是说明此时我们这样使用函数的方式是不对的呢?下面我们来看一下对于函数,我们应该如何使用;
四、函数的调用
调用函数的方式
1.传值调用
简单的理解就是将实参的值传给形参,函数的实参和形参分别占用不同的内存块,此时对形参的修改不会影响实参。
2.传址调用
- 传址调用就是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
现在我们已经了解了函数的两种调用方式,那它们又应该在哪种情形下使用呢?
3.调用情景
我们在函数中进行的操作的对象如果是实参的值,并不会改变实参本身,那我们就可以用传值调用;这个情景是不是就好比我们做的第一道题——找出两个整数中的最大值;
我们在函数中进行的操作对象如果是实参本身,在函数体内需要对实参本身进行修改,那我们就要用传址调用;这个情景是不是就是我们现在遇到的问题——交换两整型变量的值;
也就是说如果我们要解决这一道题,我们不能直接将参数的值传给函数,我们应该传送参数的地址,这时我们应该如何操作呢?现在我们需要简单介绍一下指针的相关知识点;
数据在内存中的存放;
我们在定义变量的时候就是向内存申请了一块空间来存放数据。每一个空间都有自己的地址。
取地址与解引用;
我们可以通过取地址操作符&将变量的地址给取出来存放在指针中,再通过解引用操作符*就可以将被存放的地址中的内容给取出来进行正常使用,此时解引用的对象就等价于原对象。
下面我们就尝试着借助传址调用来编写代码:
代码语言:javascript复制//写一个函数可以交换两个整型变量的内容
void swap2(int* x, int* y)//因为传送过来的是地址,所以函数需要接收地址,这里定义的就是指针类型的x、y;
{
int z = *x;//*——解引用操作符,此时的*x等价于变量a,所以这里需要有整型变量来接收a的值;
*x = *y;//*y等价于b,此时将*y赋值给*x就等价于b赋值给a;
*y = z;//最后将带有a值的z赋值给*y,也就等价于将b值赋值给a,此时完成两数交换;
}
void swap(int x, int y)//void——无返回类型;swap——函数名;x、y——参数;
{
int z = x;//函数体——交代函数如何实现
x = y;
y = z;
}
int main()
{
int a = 1;
int b = 2;
printf("a=%d b=%dn", a, b);
swap2(&a, &b);//将a、b的地址传送进函数swap2;
printf("a=%d b=%dn", a, b);
return 0;
}
在这次的代码中,我们定义了一个swap2函数,这个函数同样也是无返回类型;因为此时我们是进行的传址调用,所以我们的形参是定义的指针来接收实参的地址;在函数体中我们通过对指针进行解引用,通过辅助变量来完成对两数的交换;经过这一系列操作后,最终完成a、b值的互换。那结果会如我们所想的一样吗?我们来调试验证一下:
从监视窗口我们可以看到,此时实参已经将自己的地址和地址内存储的值传送给了形参,指针x和指针y此时存储的就是实参a和b的地址以及存储在地址内的值; 我们通过解引用操作符将地址内存储的值给取了出来,此时*x=1,*y=2; 如果我们在函数体内将这两个值进行交换,实参的值能不能被交换呢?我们继续调试;
现在函数体的内容已经执行完了,指针x和y通过解引用后成功的完成了值的交换,并且此时地址内存储的值也完成了交换,但是此时实参还是没有任何变化的,那我们接着运行又会出现什么结果呢:
这里我们可以看到,回到主函数后,a和b地址内存放的值发生了变化,此时a和b的值完成了交换。我们通过传址调用很好的完成了交换两个整型变量的值。 下面我来给大家分享一下我自己对于传值调用与传值调用的理解,以及自定义函数的执行流程;
个人理解
参数
在这个例子中参数就好比有四个人,张三、李四、王五、赵六;
实参
实参就是张三和李四,实参对应的数据就是他们今天穿的衣服,张三穿的是黑T恤,李四穿的是白T恤;
形参
形参就是王五和赵六;
实例情景
王五和张三是朋友,赵六和李四是朋友,王五觉得张三的黑T很好看,赵六觉得李四的白T很好看,于是王五去找张三,赵六去联系了李四;
传值调用
传值传参
王五和赵六分别找张三和李四要到了T恤的链接,并下了单,张三和李四提供链接的这个过程就是传值传参;
接收参数
过了几天王五和赵六收到了自己买的T恤,收快递的这个过程就是接收参数;
函数运行
王五和赵六穿了一段时间T恤后感觉还是对方的T恤比较好看,于是王五和赵六交换了自己的T恤,这个过程就是函数体内的运行过程;
回主函数
现在我们回头来看张三和李四,在这个期间他们并未有过交换T恤的举动,所以在交换完之后,张三和李四身上的T恤就没有发生改变,T恤真正被交换的是王五和赵六。
从传参到接收参数再到函数运行最后回到主函数的这一整个过程就是函数的调用过程;在这个情景下进行的是传值调用,传的这个值就是T恤的链接,所以王五和赵六的T恤只是和张三和李四的T恤是同一款式,但并不是同一件衣服;这就是为什么王五和赵六他们在进行交换衣服后,张三和李四的T恤并没有进行交换;
传址调用
传值传参
王五和赵六分别向张三和李四借了他们各自的T恤,张三和李四则把他们自己的T恤从自己家给邮递了过去,这个过程就是传址传参;
接收参数
过了几天王五收到了张三的快递,并从快递上得知了张三的住址,赵六收到了李四的快递,并在快递上得到了李四的住址,收快递的这个过程就是接收参数;
函数运行
王五和赵六穿了一段时间T恤后感觉还是对方的T恤比较好看,于是王五和赵六交换了T恤,这个过程就是函数体内的运行过程;
回主函数
在穿过一段时间后,王五又按照张三的地址将衣服给邮寄回了张三,赵六则是按照李四的地址将衣服邮寄回了李四,此时张三收到的是王五交换后的T恤也就是李四的白T,而李四收到的则是王五的黑T,所以此时张三和李四手里的T恤也完成了交换;
从传参到接收参数再到函数运行最后回到主函数的这一整个过程就是函数的调用过程;在这个情景下进行的是传址调用,传的这个值是张三和李四自己的衣服以及他们自己的住址,所以王五和赵六收到的衣服就是张三和李四自己的衣服,并且他们在完成交换后将交换后的衣服分别给回了张三和李四,这就是为什么王五和赵六他们在进行交换衣服后,张三和李四的T恤也一并完成了交换;
以上就是我自己对于函数的传值调用与传址调用的理解,希望这个理解对大家有帮助。接下来我们继续介绍函数的其它使用方式;
五、函数的嵌套调用与链式访问
在数学中我们学过的函数除了常见的函数:常、指、幂、对、三角、反三角等函数外还有复合函数——f(g(x)),在咱们的C语言中对函数的使用同样除了自定义单一的函数外,还能将函数组合起来使用,也就是相互调用,这种调用方式就是咱们现在要探讨的嵌套调用与链式访问。
嵌套调用
理解:我对嵌套调用的理解就是在函数体内调用其它的函数。这里可能不太好理解,下面我来举例说明:
代码语言:javascript复制//函数嵌套调用
//打印hello
void p()
{
printf("hellon");
}
//完成三次打印
void three_p()
{
for (int i = 0; i < 3; i )
{
p();//在函数three_p中嵌套了函数p来实现函数three_p;
}
}
int main()
{
three_p();
return 0;
}
在这个代码中,我们定义了两个无返回类型的函数——负责打印的p函数以及进行循环的three_p函数,我们为了实现三次打印,于是在three_p函数中又调用了p函数,这个在函数中调用其它函数的过程就是函数的嵌套调用;
接下来我们来看一下,现在函数能不能如我们所想的一样正常运行;
从运行结果中我们可以看到,此时函数是能够正常运行的。仔细观察的话我们可以发现,其实对于主函数来说,我们在主函数中调用three_p这个函数也属于函数的嵌套调用。
链式访问
理解:把一个函数的返回值作为另一个函数的参数。链式访问与嵌套调用的区别就是,嵌套调用是在函数体内进行函数调用,而链式访问是在函数的参数内进行函数调用。下面我们通过一个例子来进一步理解:
代码语言:javascript复制//函数的链式访问
int sum(int x, int y)
{
int z = x y;
return z;
}
int main()
{
int a = 1, b = 3;
printf("%dn", a b);
int c = sum(a, b);
printf("%dn", c);
printf("%dn", sum(a, b));
return 0;
}
这个代码中我们可以看到,我们在第一次打印中,printf的参数是式子a b,第二次打印中printf的参数是局部变量c,第三次打印中printf的参数是自定义函数sum,接下来我们看看打印的值会不会有什么不同:
从打印结果我们可以看到,打印的值是一样的,我们通过这个例子可以得到一个结论:
函数的参数可以是表达式、变量以及函数。
当一个函数作为另一个函数的参数时,就可以说是另一个函数通过链式访问调用了这个函数。在理解了嵌套调用和链式访问后,咱们来编写一个可以进行两数的四则运算的代码,将函数的参数、调用以及嵌套和链式访问都归纳在内:
代码语言:javascript复制//两数相加
int sum(int* a, int* b)//int——返回类型;sum——函数名;a,b——函数形参
{
if (*a < *b)//函数体——函数的实现;
{
int c = *a;
*a = *b;
*b = c;
}
int d = *a *b;
return d;
}
//两数相减
int sub(int a, int b)
{
int c = a - b;
return c;
}
//两数相乘
int mul(int a,int b)
{
int c = sum(&a, &b) * sub(a, b);//函数的嵌套调用;
return c;
}
//两数相除
int division(int a, int b)
{
int c = sum(&a, &b) / sub(a, b);
return c;
}
int main()
{
int x, y;
scanf("%d%d", &x, &y);
int n = sum(&x, &y);//函数的实参传址调用;
printf("x y=%dn", n);
printf("x-y=%dn", sub(x, y));//函数的链式访问
int z = mul(x, y);//函数的实参传值调用;
printf("(x y)*(x-y)=%dn", z);
printf("(x y)/(x-y)=%dn", division(x, y));
return 0;
}
下面我们来看一下结果:
这里解释一下为什么是这个结果,因为我们在sum函数中使用的是传址调用,在sum函数体内我们对x、y进行了比较,完成了x与y的交换,所以此时交换完x=3,y=1,这也就是为什么虽然我们输入的是1和3但是结果却都是正数。 现在我们就介绍完了函数调用的全部内容,下面我们来看一下对于自定义函数来说最重要的部分,函数的声明和定义;
六、函数的声明与定义
函数声明
定义: 函数声明就是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。 特点:
1.函数的声明一般出现在函数的使用之前。要满足先声明后使用。2.函数的声明一般要放在头文件中的。
函数定义
定义: 函数的定义是指函数的具体实现,交代函数的功能实现。
可能对这两个定义不太好理解,下面我们把刚刚两数四则运算的例子通过函数声明和函数定义的形式再给各位演示一遍:
我们首先在函数的功能模块中定义了四个函数,每个函数都有自己的功能,这个过程就是函数的定义;
此时我们在头文件中对函数进行了声明。这里要注意的是,我这里的声明和函数的定义是分开的,我在头文件中可以进行声明,但是不能保证函数一定存在;
我们要引用刚刚定义的函数时,只需要引用我们创建的具有函数声明的头文件,此时使用双引号来进行引用,引用完头文件,我们就能正常使用自己定义的函数了。
函数的声明和定义内容比较简单,但是很重要,因为不管是在新开的项目中还是在主函数这个项目中如果要使用自定义函数,那么函数的声明和定义就是必不可少的。 注意:我们在使用自定义函数时一定要在使用前先对函数进行声明才能使用。 接下来我们来介绍一下函数篇章中比较难的一个知识点——函数递归;
七、函数递归
1.什么是递归
定义
程序调用自身的编程技巧称为递归。
递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:大事化小
理解
对于函数递归我的理解是一种特殊的函数嵌套。在我们介绍函数的嵌套使用时有提到过,一个函数在自己的函数体中调用其它函数,这就是函数嵌套;函数递归类似于函数嵌套,也是一个函数在函数体中调用函数,这不过这一次调用的函数是自己,这种嵌套方式也就相当于数学中的复合函数f(f(x))。
最简单的递归
为了方便理解,我们来举一个最简单的函数递归:
代码语言:javascript复制//函数递归
//最简单的函数递归
int main()//int——返回类型;main——函数名
{
printf("hellon");//函数体——函数的实现;
main();//调用函数,函数为自己本身——函数递归;
return 0;//return 0——函数返回值;
}
这样能不能运行呢,我们可以看一下:
可以看到我们这个程序是可以正常运行的,而且还会陷入死循环,但是它和死循环又不同,我们可以看到,它循环到一定阶段就终止了。为什么会这样呢?这里我们要拓展一个知识点——内存
内存
计算机的内存就好比与一个空间,它里面有三个分区,分别是栈区、堆区和静态区。
- 栈区里存放的是局部变量以及函数的形参;
- 堆区里存放的是动态开辟的内存,如malloc、calloc这样的函数所开辟的空间都是在堆区内开辟的;
- 静态区存放的是全局变量以及由static修饰的变量。
正如我们从上图中看到的,既然内存是一个空间,那它肯定是有一定大小的,更不用说这三个小空间了,它们也是有一定容量大小的。我们在调用函数的时候,就相当于在各自对应的小空间里继续申请空间,来存放函数的相关内容。在这个最简单的函数递归中,计算机会不停的重复一件事,就是在栈区为printf以及main函数申请空间来进行操作,每次调用main函数就会申请一块空间,每次调用printf也会申请一块空间,当程序执行的足够多时,栈区的空间就被全部申请完了,此时就没法继续申请空间来运行程序了,这种情况也被称为栈溢出。这也就是为什么这种递归方式会使计算机陷入死循环,但又会有一个停止点。
2.递归的两个必要条件
函数递归在使用时有两个必要条件:
(1)使用递归时,需要附加限制条件,防止代码进入死循环导致栈溢出;(2)每次递归调用之后,应该越来越接近这个限制条件;
对于递归来说,这两点也就是递归在使用时的两个必要条件,只有这两个必要条件同时满足,函数才能正常递归。
3.递归与迭代
迭代:
就是重复的去做一件事情,也就是循环。
理解:
我对于迭代的理解,就是在函数体内使用循环。我们在探讨函数的嵌套调用的时候有提过,所谓的嵌套就是在函数体内调用函数。到递归的时候,我也提过,递归就是一种特殊的函数嵌套,只不过这时嵌套的函数是它本身。迭代也就是在函数体中通过使用循环来让函数重复的做一件事。可能不太好理解,怎么我们在将函数,你这里又是函数嵌套又是函数递归,现在又说迭代,咋又提到了循环呢?没关系,下面我们通过题目进一步理解:
求n!
这一题大家还有印象吗?还记不记得我们在哪里提到过?没错我们在分支与循环篇章的习题演练二就有详细介绍过求n的阶乘,有兴趣的朋友可以点击链接回顾一下求解的过程,这里我就不再重复了,下面直接进行代码编写;
代码语言:javascript复制//函数的递归与迭代
// 题目1:求n的阶乘(不考虑溢出)
//正常编写:
int main()
{
int n = 0;
int j = 1;
scanf("%d", &n);
for (int i = 1; i <= n; i )
{
j *= i;
}
printf("%d!=%dn", n, j);
return 0;
}
现在我们通过正常编写,在主函数内借助循环来完成对n的阶乘的求解,下面输入5来测试结果:
接下来我们尝试通过递归来实现一下n的阶乘,编写前我们需要了解一下,我们在求n的阶乘的时候,有这么一个公式:n=1,n!=1;n>1,n!=n*(n-1)。在知道了这个公式后,我们再来进行编写:
代码语言:javascript复制//通过函数递归完成n的阶乘;
int fac(int a)
{
if (a==1)
{
return 1;
}
else
{
return a * fac(a - 1);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("n!=%dn", fac(n));
}
下面还是用5来测试结果:
这里可以看到,通过递归的好处就是我们将复杂的问题简单化了,原本是要求n的阶乘,通过递归后变成了求n*(n-1),最后我们来通过函数迭代来完成求n的阶乘:
代码语言:javascript复制//通过函数迭代完成n的阶乘;
int fac(int a)
{
int b = 1, c = 1;
for (b; b <= a; b )
{
c *= b;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("n!=%dn",fac(n));
return 0;
}
我们来看一下运行结果:
看到这个代码,大家有没有什么感受啊,貌似跟我们直接编写的代码大差不差的,只不过原先是在主函数中使用了循环,现在是在自定义函数中使用了循环,这里我要说明的就是,这种方式就是迭代。
通过这个例子,不知道大家有没有那种醍醐灌顶的感觉。有朋友可能就会说了,既然迭代就是在函数体中使用循环,那为什么不直接在主函数体中使用循环呢?这样不是更简洁一点吗?这个问题我是这么理解的:
- 首先,我们知道,在函数体中使用循环的这种方式就叫做迭代,那么在我看来在主函数体中使用循环也是迭代;
- 其次,我们在编写像现在的这些代码时有一点肯定的是,直接在主函数中编写会更简洁一点,因为我们编写的内容都是比较简短的,但是咱们想象一下,如果我们工作了,有一天需要编写代码时大量重复的使用求n的阶乘这个功能,那是不是意味着我们每使用一次,就要对这个功能编写一次?但是如果我通过定义函数来完成,那在进行复数使用时,我们是不是只需要调用一下函数就可以了;
- 最后,函数的作用就是简化代码,代码复用。
结语
C语言总集篇——函数的全部内容到这里咱们就全部介绍完了,希望这篇内容能帮助大家更好的学习和复习函数的相关内容。
各位如果喜欢博主的内容,还请来一套点赞、关注、转发三连招。
今天的内容到这里就全部结束了,接下来我会继续给大家分享C语言学习的相关知识点,咱们下一篇再见。