目录
1.函数是什么
数学中我们常见到函数的概念。但是你了解 C 语言中的函数吗?维基百科中对函数的定义: 子程序在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。
2.c语言中函数的分类
1. 库函数2. 自定义函数
2.1.库函数
2.1.1.如何学习查找库函数
为什么会有库函数?1. 我们知道在我们学习 C 语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf )。2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作( strcpy )。3. 在编程是我们也计算,总是会计算 n 的 k 次方这样的运算( pow )。像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C 语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
简单的总结, C 语言常用的库函数都有:IO 函数字符串操作函数字符操作函数内存操作函数时间 / 日期函数数学函数其他库函数
2.1.2.一些库函数演示 (参照文档,学习几个库函数)
1.strlen函数
网站上的介绍:
代码示例:
代码示例:
注:memst中的arr是数组名,代表数组首元素的地址。
2.2.自定义函数
2.2.1.自定义函数介绍
如果库函数能干所有的事情,那还要程序员干什么?所以更加重要的是 自定义函数 。自定义函数和库函数一样,有 函数名 , 返回值类型 和 函数参数 。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
基本组成:
ret_type fun_name ( para1 , * ){statement ; // 语句项}ret_type 返回类型fun_name 函数名para1 函数参数注:1.函数可以没有参数和返回值,若不需要返回值,则返回类型用void代替。2.函数设计应该追求高内聚低耦合(自己完成自己的事情,尽量不和其他函数有什么联系)3.函数参数不易过多。4.设计函数时,尽量做到谁申请的资源就由谁来释放(后期再讲)
2.2.2.函数应用例子(※)
1.写一个函数可以找出两个整数中的最大值。
代码:
2.写一个函数可以交换两个整形变量的内容。
错误代码:
错误代码的调试:
正确代码:(借助于指针)
注:
1.从这里我们可以看出,执行函数时,系统会给形式参数重新创建地址(而不是调用函数时里面的实参地址),系统只是把函数调用里的实参数值赋给了对应的形参。上面错误代码中只是把x和y两个形参的数值交换了,因为不是同一块地址,所以不影响a和b的值。
简单地说就是:当函数调用时,实参传给形参,形参其实是实参的一份临时拷贝,所以对形参的修改不会影响实参。
3.函数的参数
3.1.实际参数(实参)
真实传给函数的参数,叫实参。实参可以是: 常量、变量、表达式、函数 等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
3.2.形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
4.函数的调用
4.1. 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2. 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
注:
1.因为传数组时其实传的是数组首元素地址,所以使用数组进行传递时也相当于是传址调用。
2.使用全局变量可以实现传址调用的功能,但是不建议使用。
4.3.练习(※)
1. 写一个函数可以判断一个数是不是素数。2. 写一个函数判断一年是不是闰年。3. 写一个函数,实现一个整形有序数组的二分查找。4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。5.借助函数实现:如果满足某条件(n=5)输出hehe,否则输出haha。
练习题2:
练习题3:
错误代码:
错误代码运行结果:
正确代码:
正确代码运行结果:
注:数组arr传给binary_search的时候,其实传递的是arr数组首元素的地址,像这种数组在传参的时候,这种元素个数一定是在外面算好之后再传进来的。
练习题4:
正确代码1:
正确代码2:
正确代码3:
错误代码:
练习题5:
注:函数中,若想让函数提前结束,可借助于return实现,如上代码,有返回值的函数使用return加返回值实现,无返回值的,只用一个return便可实现。
5.函数的嵌套调用和链式访问
5.1.嵌套调用
注:
1.这里面的调用关系是:main函数调用three_line,three_line里面调用new_line。函数是可以相互调用的,即嵌套调用。
2.函数可以嵌套调用,但是不能嵌套定义。如下图所示就是嵌套定义的例子,是错的 。所有的函数必须都是在同一个层次上。
5.2链式访问
把一个函数的返回值作为另外一个函数的参数。
知识点2:
代码:
运行结果:
注:如下图MSDN对printf函数的介绍,printf函数返回值的是该函数在屏幕中打印字符的个数(若在打印中发生错误,返回一个负数)
6.函数的声明和定义
6.1.函数的声明和定义
函数声明:1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。3. 函数的声明一般要放在头文件中的。函数定义:函数的定义是指函数的具体实现,交待函数的功能实现。
函数直接在函数使用前定义:
6.2.实际工程中编写的函数代码
过程:一个公司不会编写add代码需要一个程序员编写,那么程序员需要编写add.c文件和add.h文件然后将两文件所在项目进行静态库加密(二进制的),再将add.h头文件(头文件相当于说明书,让公司知道函数参数是什么,知道该怎么用这个函数)和静态库加密的add.lib文件卖给公司(不能卖add.c文件,要是卖出add.c文件,公司就知道函数实现代码,程序员就没法卖给其他公司了),公司导入add.h文件和声明add.lib文件配合自己的主函数代码便可实现。
1.程序员要写的代码:
3.将.h和.lib文件卖给公司
7.函数递归
7.1.什么是递归
程序调用自身的编程技巧称为递归( recursion),函数自己调用自己就是函数递归。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小
简单的递归代码(是递归代码,但是会死递归):
运行结果:死循环(图中程序崩溃了,所以停止)
程序的报错:
注:stack overflow是栈溢出的意思,递归代码中非常常见的错误。
7.2.递归的两个必要条件(练习)(※)
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。2.每次递归调用之后越来越接近这个限制条件。注:不满足条件就会无线递归,出现栈溢出的现象。
练习一:
接收一个整型值(无符号),按照顺序打印它的每一位。例如:输入:1234 输出 :1 2 3 4
代码:
注:
1.%u代表无符号整数的意思。
2.若没有if语句进行限制,就会和前面的main一样,无限的递归下去,最后出现栈溢出(stackoverflow)的现象,导致程序崩溃。(有一个外国网站就叫stackoverflow相当于程序员的知乎,有不会的就可以在上面提问)Stack Overflow - Where Developers Learn, Share, & Build Careers
栈溢出:每一次函数调用都会在内存的栈区申请一块空间,若无线的递归下去,栈空间的内存会被用完,没有空间可以被开辟了,就会出现栈溢出的现象。
练习二:
编写函数不允许创建临时变量,求字符串的长度。(意思就是写一个函数实现函数strlen函数的功能)
代码1:(其实该代码创建了count临时变量,不满足题目的要求)
代码2: (符合题意的代码)
代码2图形解释:
7.3.递归与迭代(练习)(※)
求n的阶乘。(不考虑溢出)
递归实现的代码:
求第n个斐波那契数。(不考虑溢出)斐波那契数:1 1 2 3 5 8 13 21 34 55......
注:此处这个递归系统重复进行了很多计算,效率很低,比如下图,计算第40个斐波那契数时,第三个斐波那契数被计算了39088169次。
8.函数递归的几个经典题目(自主研究)(※)
1. 汉诺塔问题2. 青蛙跳台阶问题
8.1.汉诺塔问题
题目:相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void move(char pos1, char pos2)
{
printf("%c->%c ", pos1, pos2);
}
void Hanoi(int n, char pos1, char pos2, char pos3)
{
if (n == 1)
{
move(pos1, pos3);
}
else
{
Hanoi(n - 1, pos1, pos3, pos2);
move(pos1, pos3);
Hanoi(n - 1, pos2, pos1, pos3);
}
}
int main()
{
int n = 0;
printf("请输入盘子数:");
scanf("%d", & n);
Hanoi(n, 'A', 'B', 'C');
return 0;
}
运行结果:
解析:
从该问题我们可以看出,在汉诺塔问题中:A杆为起始杆,B杆为中转杆,C杆为目的杆。
8.2.青蛙跳台阶问题
题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法?(先后次序不同算不同的结果)。
代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int fib(int n)
{
if (n == 1)
return 1;
else if (n == 2)
return 2;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
printf("请输入阶数:");
scanf("%d", &n);
int s = fib(n);
printf("总共有%d种跳法\n", s);
return 0;
}
运行结果:
解析:
题目换一种说法,就是一个数n,让用1和2经过不同的排列来组成。由于排列顺序的影响,对于稍微大一点的n来说,都有很多种。
n=1,只有一种跳法,[1].
n=2,那么有两种跳法,[2],[1,1].
n=3,那么有三种跳法,[1,1,1],[1,2],[2,1].
n=4,那么有五种跳法,[1,1,1,1],[1,1,2],[1,2,1],[2,1,1],[2,2].
n=5,那么有八种跳法,[1,1,1,1,1],[1,1,1,2],[1,1,2,1],[1,2,1,1],[2,1,1,1],[2,2,1],[2,1,2],[1,2,2].
我们发现,当n依次加1时,它的组合数刚好构成了一个斐波那契数列(即某一个数的大小,等于它前面两个数之和)。
深度剖析:
假设n个台阶,有f(n)种走法。青蛙第一次可以跳1个台阶或者2个台阶,跳一个台阶的话剩下n-1个台阶有f(n-1)种走法,跳2个台阶的话剩下n-2个台阶有f(n-2)种走法。
推出:走n个台阶总共有f(n-1)+f(n-2)种走法。
补充:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int fib(int n)
{
if (n == 1)
return 1;
else if (n == 2)
return 2;
else if (n == 3)
return 4;
else
return fib(n - 1) + fib(n - 2) + fib(n - 3);
}
int main()
{
int n = 0;
printf("请输入阶数:");
scanf("%d", &n);
int s = fib(n);
printf("总共有%d种跳法\n", s);
return 0;
}