1.函数的定义
2.函数的分类
3.函数的参数
4.函数的调用
5.函数的嵌套调用和链式访问
6.函数的声明与定义
7.函数的递归
1.函数的定义
维基百科对函数的定义:
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。
2.函数的分类
(1)库函数
(2)自定义函数
2.1库函数
早期的c语言没有库函数,当不同的人想表达同一种功能时,需要自己写相应的代码,这时会造成代码冗余,开发效率低,不标准的问题,为了解决这种问题,我们把常用的一些功能实现成函数,集成为库,由C语言直接提供--库函数就出现了。
这个网站可以看c语言的库函数 www.cplusplus.com
也可以用MSDN软件查看库函数
2.2自定义函数
库函数只能实现一些简单的功能,并不能实现所有功能,更加重要的是自定义函数
自定义函数与库函数一样,有函数名,返回值类型与函数参数
函数的组成
//返回类型 函数名 函数参数
ret_type fun_name(para1,*)
{ //大括号内为函数体
statement;//语句项
}
例1:写一个函数求两个整数的最大值
int MAX(intx,inty)
{
if(x
例2:写一个函数,交换两个整数
void swap(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
int main()
{
int a, b;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
此代码无法完成交换,因为swap里的x,y是形参,而主函数里的a,b是实参,主函数中进入swap函数后,实参的值被赋给了形参,然后形参实现了转换,但是形参与实参的存放地址不同,所以形参虽然完成了交换,但是实参没有,所以此无法完成任务。
即--当实参传给形参时,形参是实参的一份临时拷贝,对形参的修改不会影响实参。
所以,当我们要实现上述功能,我们可以把a,b的地址传给swap函数,当我们解引用a,b的地址,就可以通过解引用完成对a,b的修改,代码如下图
void swap(int* pa, int* pb)
{
int tmp = *pa;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a, b;
scanf("%d%d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
所以,当swap如果想对实参的值产生影响时,需要与main函数建立联系,就需要把实参的地址传给函数
3.函数的参数
3.1实际参数(实参)
真实传给函数的参数
可以是:常量,变量,表达式,函数等。但无论实参是什么类型的量,在函数调用时都必须有确定的值。
3.2形式参数(形参)
形式参数是函数括号中的变量,因为形参只有在函数被调用的时候才实例化(分配内存单元),所以叫形式参数。函数调用完自动销毁,因此形参只在函数中有效。
4.函数的调用
4.1传值调用
直接把函数的值传过去,形参和实参分别占用不同的内存块,形参的改变不会影响实参。像上面的第一种用法。
4.2传址调用
把函数外部创建的变量的地址传递给函数参数的调用方式,像上面的第二种用法。
这种传参方式可以使函数内部与外部的变量建立联系,可以在函数内部可以直接操作函数外部的变量
但是两种调用的本质其实都是传值调用。
练习:写一个函数可以用二分法查找数组中的元素
//创建一个函数,用二分法寻找数组中的元素
int search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left < right)
{
int mid = left + (right - left) / 2;
if (arr[mid] < k)
left = mid;
else if (arr[mid] > k)
right = mid;
else
return mid;
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
scanf("%d", &k);
int ret=search(arr, k, sz);
if (ret >= 0)
printf("找到了,下标是:%d\n", ret);
else
printf("找不到\n");
return 0;
}
注意:数组在传参的时候传的不是整个数组,而是数组首元素的地址。所以上述代码的数组长度不能在函数体里求。
5.函数的嵌套调用和链式访问
5.1嵌套调用
函数可以嵌套调用,不能嵌套定义
//可以嵌套调用
int test1()
{
}
int test2()
{
test1();
}
int main()
{
test2();
return 0;
}
//不能嵌套定义
int main()
{
int test()
{
}
return 0;
}
5.2链式访问
把一个函数的返回值作为另一个函数的参数
int main()
{
int len=srtlen("abcd");
printf("len=%d\n",len);
return 0;
}
在上图中,我们可以把函数strlen的返回值作为函数printf的参数,即链式访问
int main()
{
printf("len=%d\n",strlen("abcd"));
return 0;
}
分析下列代码打印的值?
printf("%d",printf("%d",printf("%d",43)));
函数printf的返回值类型为int,返回值为打印元素的个数,所以上述代码的打印结果为:4321
6.函数的声明与定义
6.1函数的声明
#include
//函数的声明
//形参的名字可以省略,只留类型
int ADD(int x,int y); //int ADD(int ,int );
int main()
{
int a=10;
int b=20;
int sum=ADD(a,b);
printf("%d",sum);
}
//函数的定义
int ADD(int x,int y)
{
return x+y;
}
如上图,当函数在主函数下面时,编译器从上往下读取代码,当我们要调用ADD函数时,虽然我们定义了ADD函数,但是编译器还没有找到,所以会报错,为了防止这种情况,我们要在代码前面先声明我们要调用的函数,声明方法如上图。
(1)函数的声明是告诉编译器有一个函数叫什么,参数是什么,返回值是什么。但是函数的声明不能决定函数是否存在。
(2)函数的声明一般在函数的使用之前,要先声明后使用
(3)函数的声明一般放在函数的头文件中
6.2函数的定义
函数的定义指函数的具体实现。
上面例子当我们把ADD函数的声明放在头文件,ADD函数的定义放在源文件是,即ADD.h和ADD.c构成了一个加法模块。
// ADD.h
#ifndef __TEST_H__
#define __TEST_H__
int ADD(int,int);
//定义函数,并说明该函数的额使用方法
#endif //__TEST_H__
// ADD.c
int ADD(int x,int y)
{
return x+y;
}
#include
#include "ADD.h" //导入头文件
int main()
{
int a=10;
int b=20;
int sum=ADD(a,b);
printf("%d",sum);
return 0;
}
当我们不想让别人知道自己的函数是如何实现的,可以把该模块编译成静态库
点击项目名称--右击属性--常规--配置类型--静态库(.lib)--编译
之后会生成一个.lib的文件,同样可可以在主函数中使用
#include
#include "ADD.h" //导入头文件
#pargma comment(lib,"ADD.lib")//导入静态库
int main()
{
int a=10;
int b=20;
int sum=ADD(a,b);
printf("%d",sum);
return 0;
}
导入自己写的头文件时要用""不能用<>。
库函数的使用方法与静态库是类似的,只是每次我们创建文件时编译器帮我们导入了他们的静态库,不需要我们再导入。
7.函数的递归
7.1什么是递归
程序调用自身的编译技巧
递归作为一种算法被广泛运用。
一个过程或函数在其定义或说明中直接或间接的调用自身的一种方法,它通常把一个大型复杂的问题转化成与原问题相似的较小的问题,只需要程序就可以描述出解题过程中所需的多次重复计算,大大减少了程序的代码量,其思考方式在于:大事化小
当我输入一个数,要把这个数的每一位按原本的顺序输出
思考过程:我们可以写一个print函数,用一个参数n,用来打印参数的每一位。当我们输入‘1234’时,调用函数print(1234),在我们打印1234这个数时,我们发现用1234%10就可以得到最后一位4,这时我们可以用print打印123,再单独打印4。打印123时,我们又可以用123%10得到最后一位3,然后我们再用print打印12,再单独打印3 4以此类推,最后只剩一位的时候,我们可以用print打印1,再单独打印2 3 4。通过上述思考过程,我们就可以着手写代码了。
void print(int n)
{
if(n>9)
print(n/10);
printf("%d",n%10);
}
int main()
{
unsigned int n;
scanf("%d",&n);
print(n);
return 0;
}
上述代码即使用了递归的方法打印输入的数,当我们输入1234时,我们首次调用print函数,进入print函数后,首先判断n是否大于9,发现符合情况,我们把n/10在传入print函数中进行第二次调用,这时n为123,然后重复上面的操作,不停的调用print函数,直到n<9时,不再符合情况,所以我们不再调用print,而是进入printf函数,输出完成后,最后的一次调用结束,程序进入倒数第二次调用的printf函数,然后依次打印,最后回到主函数,程序结束,完成打印。
通过上面的过程分析,其实我们可以把递归理解成递推与回归,在上面的例子中,我们先通过不断的调用,进行递推,当达到某一条件时,我们再一步步的回归回来。
我们再做一道例题:用递归的方法求一个字符串的长度。
思考:我们知道,字符串传参的时候并不是把整个字符串传过去,而是传第一个元素的地址,所以,函数接收的应该是一个指针变量类型,然后输出的长度为一个整形。当调用到求长度函数时,先指到第一个元素,然后我们的长度加一,再求后面几个元素的长度,以此类推,知道我们找到字符串的结束标志‘\0’,计数结束,下图为代码。
int my_strlen(char* str)
{
if(*str!='\0')
return 1+my_strlen(str+1); //指针后面加一代表把指针的位置往后移一位
else //前面加一代表每次调用数值加一
return 0;
}
int main()
{
char arr[]="abcd";
int len=my_strlen(arr);
printf("%d\n",len);
return 0;
}
7.2递归的两个必要条件
注意:我们每次调用函数时,都会在内存的栈区上开辟一块新的空间来存储这次调用使用的各种量,函数调用完被销毁。所以,当我们不停的调用函数,当内存的额栈区被储存满的时候,程序就会报错,造成栈溢出。为了防止这种情况,我们写递归时有两个必要条件。
(1)存在限制条件,当满足这个限制条件时,递归便不再继续。
(2)每次递归调用后都会越来越接近限制条件。
7.3递归与迭代
我们可以把迭代简单的理解为循环,我们发现,解决很多问题时,递归与迭代两种方法都可以使用,那么我们如何判断该使用哪种方法呢?我们看下面的例题
求第n个斐波那契数(斐波那契数:从1 1开始,前两个数的和等于第三个数)
设该函数为fib,当n<=2时,fib=1
当n>2时,fib(n)=fib(n-1)+fib(n-2)
我们发现该数的定义非常符合递归的方法,代码如下。
int fib(int n)
{
if(n<=2)
return 1;
else
return fib(n-1)+fib(n-2);
}
int main()
{
int n=0;
scanf("%d",&n);
int ret=fib(n);
printf("%d\n",ret);
return 0;
}
通过上述代码,我们可以实现计算第n个斐波那契数的任务,但是我们发现,当我们输入的n比较大时,计算机的计算速度会变得非常缓慢,我们来分析这个代码就可以发现,我们计算第n个数时,我们要先计算第n-1和第n-2个数,计算第n-1个数时,要先计算第n-2和第n-3个数,计算n-2个数要先计算第n-3和n-4个数,这样分析,当我们输入的n比较大的时候,会产生大量的多余的重复计算,会使得效率变得极低,说明该问题使用递归的方法存在缺陷。
那我们可以考虑使用循环的方法,代码如下。
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;
}
int main()
{
int n=0;
scanf("%d",&n);
int ret=fib(n);
printf("%d\n",ret);
return 0;
}
计算第n个斐波那契数时,我们需要三个数a,b,c。再n小于2时,c=1,当n大于2时,我们逐次计算,计算一次,a b c三个数往后移一位,依次计算,得到最终结果。
运行后发现,使用循环的方法来计算的效率大大提高了,由这个例子我们可以得知,并不是在解决所有的问题时递归都比循环的方法好,当我们发现在解决一个问题时,使用递归的方法比较简单,并且没有缺陷时,我们可以使用递归的方法,但是当我们发现使用递归的方法解决问题存在缺陷时,这时无论使用循环的方法有多复杂,我们都只能使用循环的方法。