作者:低调 作者宣言:认真写好每一篇博客 作者gitee:link
🧡前言
各位读者们大家好,今天我又来更新好文了,今天我们来讲关于函数的知识,通过简单的函数来告诉大家函数怎么使用,在讲函数用时的注意事项,并且在讲函数的递归,函数对我们的重要性,话不多说,接下来进入正文。
一、函数是什么?
数学中我们常见到函数的概念。但是你了解C语言中的函数吗?在数学中函数是类似于y=f(x)的形式,x相当于参数,y相当于接受这个函数所返回的值,f()相当于一个函数的名字,在C语言中函数和这个有些区别,但也有相似的地方:
维基百科中对函数的定义:在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
这时候你们依旧不懂函数是什么。没关系,我们下来慢慢来介绍
二、函数的分类
1.库函数 2.自定义函数
2.1库函数
库函数是什么? 我们把C语言自己已经设计的好的函数,我们可以直接拿来用到函数就叫库函数。 为什么会有库函数?
- 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想
把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。 - 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
- 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发
我们怎么学习库函数? 介绍一个网站:link 进入网站我们就会看到这个界面,但我们最好使用一下老版本,点一下我圈的红框框,就会就如这样的界面: 看到这个界面你就可以进行函数的查找,并且在里面可以看到函数是怎么使用的,一会教大家学习两个函数的使用
这个就是C库函数,左边是对应的头文件,每个头文件里面有许多库函数,注意,我们在使用库函数的时候必须调用对应的头文件#include<>. 因为在创建C语言之初,创造者为了方便我们使用,创造许多库函数,这些函数都是别人写好的,放在对应的头文件下,那我们后人在使用他的时候就需要调用对应的头文件,相当于在告诉别人:我要使用一下你的函数,这样也体现了礼貌。
简单的总结,C语言常用的库函数都有: ~IO函数(例:输入,输出函数) ~字符串操作函数(例:strlen) ~字符操作函数(例:isupper) ~内存操作函数(例:memset) ~时间/日期函数(例:time) ~数学函数(例:pow) ~其他库函数 我们对照文档来介绍两个库函数的作用和用法:
1.strcpy
🧡第一部分:我们可以看到他的一些内容 char * strcpy ( char * destination, const char * source ); char * strcpy是他的返回值,相当于数学函数的y, char * destination, const char * source是他的参数,相当于数学函数的x,通过这个我们可以看出来,这个函数有两个参数,都为char类型,返回值也是char类型。 🧡第二部分:是介绍函数的参数是什么,以上面的为例, destination:指向要在其中复制内容的目标数组的指针。 source:要复制的 C 字符串. 意思就是把你要复制的内容复制到想要复制的字符串上。 所以这就是一个字符串复制函数。 🧡第三部分:就是介绍返回值,应该返回什么,这个函数就是饭hi目标函数的地址。 🧡第四部分:他会给你这个函数的使用案例,让你更加充分的知道这个函数怎么使用的,下面灰色的框框是输出样例。 🧡第五部分:就是刚才特别强调的要包含头文件,你每次查找一个函数的时候就会显示对应的头文件。 🧡第六部分:是告诉你与你查找的函数,还有类似的函数有哪些。
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[]="Sample string";
char str2[40];
char str3[40];
strcpy (str2,str1);
strcpy (str3,"copy successful");
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
大家可以自己下来查看文档具体在再来看看这个函数怎么使用的,但特别要注意,这个文档是英文的,我英语不好啊,看不懂文档啊,不要当心,做事需要一点一点的来,不会的单词可以查,是在不行,我们采用一些翻译软件啊,也是可以的。
2.memset(内存设置函数)
这个我就不具体给大家介绍了,你们可以对照第一个例子去学学怎么使用。不懂的可以在评论下讲出来,作者会及时回复的。
2.1.1查询工具
MSDN(Microsoft Developer Network)离线版 link link(英文版) link(中文版)
2.2自定义函数
如果库函数能干所有的事情,那还要程序员干什么? 所有更加重要的是自定义函数。 自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。 函数的组成:
ret_type fun_name(para1, * )
{
statement;
}
ret_type
fun_name
para1
我们举个例子: 写一个函数可以找出两个整数中的最大值。
#include <stdio.h>
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;
}
我们可以看到该函数实现了比较了两个数的大小,为什么可以呢? 我们通过get_max(num1,num2)将我们要比较的数字告诉这个函数,这个函数才能知道要比较哪两个数,int get_max(int x, int y)通过x,y来接收我们所传过来的num1,num2,我们所接受的变量类型也应该要和所传的参数类型保持一致,x,y得到了两个数,函数体里进行比较,将最大的数字返回出来用max接收,将其打印出来就好了。
通过这个函数,我才读者大致应该有了一些自定义的函数了解了。
那我们再来看一个例子: 写一个函数可以交换两个整形变量的内容。
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap1(num1, num2);
printf("Swap1::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
我们看到运行结果,没有达到我们想要的结果。这是为什么呢?
这里需要补充一点:void Swap1(int x, int y)x,y我们可以叫函数的形式参数, Swap1(num1, num2);num1,num2我们叫他实际参数,真是传递给函数的参数。我们把num1,num2传给形参,形参相当于实参的一份临时拷贝,x,y在内存中又开辟了一块新的空间来存放num1,num2的值,我们在函数体里执行完程序后,出这个函数后,x,y就自动被销毁了,外面的实参并没有得到改变。所以达不到我们想要的结果。那我们应该怎么达到我们想要的结果呢?
先看代码:
#include<stdio.h>
void Swap2(int *px, int *py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int num1 = 1;
int num2 = 2;
Swap2(&num1, &num2);
printf("Swap2::num1 = %d num2 = %d\n", num1, num2);
return 0;
}
看到结果是我们想要的结果,那这次又是为什么呢? 这里介绍一个知识?内存
内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。
变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的。
#include <stdio.h>
int main()
{
int num = 10;
#
printf("%p\n", &num);
char*px=#
return 0;
}
我们把取到的地址需要放到指针变量里 Swap2(int *px, int *py),*告诉我们px是指针,int 是p指向的地址的类型。px里面存放的就是num的地址。所以我们接收的时候也要用指针变量接收。
注:%p是打印地址,我们大部分数据都是放在内存当中的,每一个数据都有自己在内存的地址相当于我们每家的地皮,我们将数据放在这个地址上,就好比在地皮上建了一个房子,我们在来看Swap2 (&num1, &num2);我们将num1,num2的地址传给形参,x,y就找到了num1,num2的地址,在进行交换数字的时候,也就将num1的地址上的内容换成了num2的内容,num2的地址上的内容换成了num1的内容,也就很好的说明Swap这个函数达不到我们想要的结果,因为x,y是重新开辟了一个空间,交换后num1,num2上的内容得不到改变。我们可以把地址比作地皮,内容比作防止,只有找到你想要修改的地址,才能改变内容。相当于我们只有找到num1,num2的地皮,才能把两家的房子进行交换,如果你是Swap1里面的x,y的地皮,你交换后,达不到想要的结果。
相信大家对我说的有一些了解了,但还是不太清楚,现在我们通过更直观的方法让大家理解: 大家可以清楚的看到了我想要表达的意思了吧 通过这两个自定义函数你们应该了解了自定义函数不是那么简单的,我们要学习的还有很多,但没有关系,人生就是在不断的学习中,我们也明白写函数很考验脑力,需要程序员自己想出来,也充分说明了创造性很强。 这样我接下来要讲的函数的参数和函数的调用相信大家应该会很容易懂了。
三、函数的参数和函数的调用
3.1 实际参数(实参)
真实传给函数的参数,叫实参。 实参可以是:常量(3)、变量(a)、表达式(3+7)、函数(get_max(3,7)他的返回值作为参数)等。 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
3.2 形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1,num2 和传给 Swap2 函数的 &num1 , &num2是实际参数。
3.3函数的传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参(Swap1是传值调用)
3.4函数的传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。(Swap2是传址调用)
四、函数的声明和定义
4.1函数的声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中
函数的声明实际就是告诉编译器函数的返回值,函数名,函数的参数,例如(int max(int x,int y);)这就是函数的声明,不做具体的操作,实现功能在函数的定义中。
4.2函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
函数体里具体实现的功能。例如: { if(x>y) return x; else return y; }
五、函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
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;
}
嵌套调用就是在别的函数体里调用其他函数,上面的例子就是在main()函数里面调用three_line()函数;在three_line()里面调用 new_line();
但是不能嵌套定义,举例:
#include<stdio.h>
int main()
{
int max(int x,int y)
{
if(x>y)
return x;
else
return y;
}
return 0;
}
这就是嵌套定义,在main()函数里面又定义了一个函数,这样是错误的,每个函数都是独立的。 结论:函数可以嵌套调用,但不可以嵌套定义!!!
5.2链式访问
把一个函数的返回值作为另外一个函数的参数。 看代码:
#include <stdio.h>
#include <string.h>
int main()
{
char arr[20] = "hello";
int ret = strlen(strcat(arr,"bit"));
printf("%d\n", ret);
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
这里作者直接告诉printf()函数的返回值是在屏幕上打印的字符个数。代码举例:
#include<stdio.h>
int main()
{
int ret = printf("%d", 43);
printf("\n%d\n", ret);
return 0;
}
我们用ret接收printf(“%d”,43);的返回值,打印出来是2,而43打印在屏幕上刚好是两个字符,所以我们知道了printf()函数的返回值是什么了吧,那就请读者自己思考上面的代码运行结果是多少了哦,这就是链式访问。
六、函数的递归
6.1什么是递归
程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小。 让作者用更加直接方法带你们深度理解递归。
6.2练习来深度理解递归
一、练习1: 接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4 我们看到这个图,常规的办法使解决不了这个问题的,但我们已经知道问题想要表达的意思了,而且每个数字都已经取出来,只需要按照顺序打印出来即可,那我们来讲讲用法递归的方法来实现这个代码: 先看代码,然后我会用画图的方式给大家解释的
void print(int n)
{
if(n>9)
{
print(n/10);
}
printf("%d ", n%10);
}
int main()
{
int num = 1234;
print(num);
return 0;
}
红线是进入每一层递归,蓝线就是返回时做出的操作。让我们看一下运行结果 很好的解决了问题。递归就是用很少的代码,去完成很多次的重复实验,讲大事化小,
二、练习二: 编写函数不允许创建临时变量,求字符串的长度。 意思就是模拟实现strlen函数求字符串长度。 大家可以看我的这篇博客link里面详细的介绍了怎么模拟实现strlen函数的方法,包括创建临时变量法和递归法,这里作者就不详细介绍了。
让我们再来写一个最简单的递归:
#include<stdio.h>
{
printf("hehe\n");
main();
return 0;
}
这就是一个主函数自己调用自己的递归,但是这个代码是一个错误的代码,读者可以自己下来自己去运行一下这个代码,会出现栈溢出的错误,原因就是每次调用依次函数的时候,就会在栈区在开辟一块空间,而这个递归时是一个死循环,所以栈上的空间最终会被消耗完,所以会出现错误,怎么避免这个错误呢?那我们接下来就引入使用递归的两个条件。
6.3递归的两个必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。 2.每次递归调用之后越来越接近这个限制条件。 作者所圈出来的就是重要部分,当我们以后用递归解决问题发现结果不对,可以先从这两个条件检查。
6.4递归的弊端
我们再来用递归解决两个问题吧 1.求n的阶乘(不考虑溢出)但调用次数过多,没到结束条件就栈溢出了也是有可能的。
int factorial(int n)
{
if(n <= 1)
return 1;
else
return n * factorial(n-1);
}
2.求第n个斐波那契数。(不考虑溢出)
int fib(int n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
主函数就由读者自己去写了,去调用我写的这个代码就好了,如果对代码不懂的,可以按照我那样方法去画图理解。一定要去尝试把弄懂。
但是我们运行的时候会发现问题: 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。(结果要等很长时间才出来) 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
那为什么呢? 我们发现 fib 函数在调用的过程中很多计算其实在一直重复。 如果我们把代码修改一下:
int count = 0;
int fib(int n)
{
if (n == 3)
count++;
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
已经超出了int的的存储范围,变成了负数,至于为什么是负数,这里关系到数据的存储的知识,如果想知道为什么,可以自己去搜搜这方面的知识,但作者在后期也会更新关于这个知识的文章的。 我们发现count是一个特别大的数字。
那我们如何改进呢? 1.在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。 2.系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。这个我之前也解释过了。
解决办法:
- 将递归改写成非递归。
- 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
那我们用非递归的方法来解决这两个问题
int factorial(int n)
{
int result = 1;
while (n > 1)
{
result *= n ;
n -= 1;
}
return result;
}
int fib(int n)
{
int a=1;
int b=1;
int c=1;
while(n>2)
{
c=a+b;
a=b;
b=c;
n--;
}
return c;
}
自行下来调试看看。
友情说明:我们在使用递归的时候,就是把事情化小,将多次重复要做的事情用递归实现,那我们依旧可以使用循环来解决这个事情,既然循环可以解决问题,还要递归干嘛,而且递归还有好多弊端,那让我们看看递归和非递归的区别吧:
- 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
- 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
- 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开
销 相信大家看到这里对于递归是不是有了更深的理解呢,和重要性呢?
七、函数和递归的练习题
函数的几个小问题(自主·研究)
- 写一个函数可以判断一个数是不是素数。
- 写一个函数判断一年是不是闰年。
- 写一个函数,实现一个整形有序数组的二分查找。
函数递归的几个经典题目(自主研究):
- 汉诺塔问题
- 青蛙跳台阶问题
写不来可以私聊作者哦。
八、总结
这篇博客讲叙了C语言函数的用法和作用,特别说明在使用的时候应该注意那些,作者码文属实不易,目的是希望你们和作者自己都能学到知识,这才是这篇博客的意义所在。
能看到这里作者感到非常开心,第一,是希望你们在看到作者的文章后,能学到知识才是最重要的,其次,感谢你们对作者的支持,这篇博客我画了很长时间,尽力把我自己想要表达和你们想要知道的知识都讲出来了,如果有写的不对的地方,希望大佬们指正出来,如果有读者不理解的地方,可以在评论区提出来,我会尽快给你们回复的。
|