不管对于我们日常工作还是考试升学,函数都是一个考察重点,本期将为大家带来:库函数、自定义函数、函数的参数与调用、函数声明与定义、函数递归等相关知识点,本章节是个重难点,请大家务必认真学习。
提示:以下是本篇文章正文内容,下面案例可供参考
数学中我们常见到函数的概念。但是你了解C语言中的函数吗? 维基百科中对函数的定义:子程序
1.在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
2.一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库
C语言中函数的分类:
为什么会有库函数?
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
如何学习库函数:C语言函数库链接
使用示例:以strcpy为例
进入上述链接后,在下图红色箭头指向处可以查找你想要的函数
比如我这里查找一下strcpy函数,左侧的框子你可以看到string.h,这是这个函数的头文件
#include
#include
//strcpy
int main()
{
//把arr2的内容拷贝到arr1
//拷贝的前提是arr1的空间能够放的下arr2的内容
char arr1[20] = { 0 };
char arr2[] = "hello";
strcpy(arr1, arr2);
//第一个参数是目的地,第二个参数是源头
//也就说把第二个参数内容拷贝到第一个参数内
printf("%s\n", arr1);
return 0;
}
这里可能会有同学有问题:“我们把hello拷贝进arr1,那拷贝后arr1[5]的\0是我们后面拷贝进去的还是原先就有的?”
我们可以这样做一个简单的验证:我们用一堆 * 来初始化arr1,看最后arr1[5]里面放的是\0还是 *
#include
#include
//strcpy
int main()
{
//把arr2的内容拷贝到arr1
//拷贝的前提是arr1的空间能够放的下arr2的内容
char arr1[20] = "**********";
char arr2[] = "hello";
strcpy(arr1, arr2);
//第一个参数是目的地,第二个参数是源头
//也就说把第二个参数内容拷贝到第一个参数内
printf("%s\n", arr1);
return 0;
}
按f10进行调试:
使用完strcpy后,原先 * 的位置变成了\0,所以strcpy是会把\0拷贝过去的
这里也仍然是打印hello,因为%s以字符串形式打印,到\0默认就不打印了
库函数的功能很多,但是总有一些我们想自己实现的功能它库函数是没有的,这个时候就需要我们自己来写函数了。
函数的组成
返回类型 函数名(参数)
{
语句项
}
举个例子:求两数较大值
int Max(a, b)
{
return a > b ? a : b;
}
int main()
{
int a, b;
scanf("%d %d", &a, &b);
int c = Max(a, b);//Max函数会返回一个整形,我们用c来接收
printf("较大值为%d", c);
return 0;
}
实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类
型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配
内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在
函数中有效。
举个例子:交换两个整形变量值
void swap(int x, int y)//这里的x和y是形参
{//这里交换函数不需要返回值,就用void
int tmp = y;
y = x;
x = tmp;
}
int main()
{
int a = 1;
int b = 2;
printf("交换前a=%d,b=%d\n", a, b);
swap(a, b);
printf("交换后a=%d,b=%d\n", a, b);
return 0;
}
我们运行一下看看结果,发现原本应该成立的逻辑并没有成功
不着急,我们按f10进入调试(进入函数按f11):
经过调试可以发现,我们的x和y确实是交换了,但是并没有影响到a和b,这就很奇怪了啊。
我们来看看a、b和x、y各自的地址,可以发现,a、b和x、y地址都是不一样的
这样我们就可以得到一个结论:
实参a和b传给形参x和y的时候。形参将是实参的一份临时拷贝。
又因为是临时拷贝,所以形参xy的改变是不会影响实参ab的
那肯定有同学问:“那我非要形参影响到实参怎么办?”
答案是肯定有办法的,但是讲这个办法前,我先讲一下原理:
我们上面那个方法,已经知道了形参是实参的一份临时拷贝,从空间角度说,它们两个并没有任何联系,那能不能想个办法,让形参和实参建立联系呢?
我们产生联系的方法就是使用指针
void swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 1;
int b = 2;
int* pa = &a;//可以通过pa找到a
int* pb = &b;
printf("交换前a=%d,b=%d\n", a, b);
swap(pa, pb);
printf("交换后a=%d,b=%d\n", a, b);
return 0;
}
解释如下:
我们知道,可以通过a的指针pa找到a。我们把a和b的地址(指针)pa和pb作为参数传过去,那么指针px和py也就获得了a和b的地址,也就是说,px和py也有能力找到a和b了。那么我们对px和py进行解引用,得到的就是a和b本身,这样操作就实现了形参影响实参了。
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
示例如下:
void swap(int x, int y)//这里的x和y是形参
{//这里交换函数不需要返回值,就用void
int tmp = y;
y = x;
x = tmp;
}
int main()
{
int a = 1;
int b = 2;
printf("交换前a=%d,b=%d\n", a, b);
swap(a, b);
printf("交换后a=%d,b=%d\n", a, b);
return 0;
}
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起正真的联系,
也就是函数内部可以直接操作函数外部的变量。
示例如下:
void swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 1;
int b = 2;
int* pa = &a;//可以通过pa找到a
int* pb = &b;
printf("交换前a=%d,b=%d\n", a, b);
swap(pa, pb);
printf("交换后a=%d,b=%d\n", a, b);
return 0;
}
void isPrime(int x)
{
int tmp = 0;//标记是不是素数
for (int i = 2;i < x;i++)
{
if (x%i == 0)
{
printf("%d不是素数\n",x);
tmp = 1;
break;
}
}
if (tmp == 0)
{
printf("%d是素数",x);
}
}
int main()
{
int a = 0;
printf("请输入一个数:");
scanf("%d", &a);
isPrime(a);
return 0;
}
#include
#include
void isPrime(int x)
{
int tmp = 0;//标记是不是素数
for (int i = 2;i < sqrt(x);i++)//sqrt需要引用头文件math.h
{
if (x%i == 0)
{
printf("%d不是素数\n",x);
return;//void就不返回东西,直接return
//return之后结束这个函数所有东西
}
}
printf("%d是素数",x);//上面没有return掉,说明是素数
}
int main()
{
int a = 0;
printf("请输入一个数:");
scanf("%d", &a);
isPrime(a);
return 0;
}
void isLeap(int x)//判断是不是闰年
{
if ((x % 4 == 0) && (x % 100 != 0))
{
printf("%d是闰年", x);
return;
}
else if (x % 400 == 0)
{
printf("%d是闰年", x);
return;
}
else
{
printf("%d不是闰年", x);
return;
}
}
int main()
{
for (int i = 1000;i <= 2000;i++)
{
isLeap(i);
}
return 0;
}
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else {
return mid;
}
}
return -1;//输入的数不在数组范围内
}
int main()
{
//二分查找的前提是有序
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;//假设要找的数为k
printf("请输入要查找的数:");
scanf("%d", &k);
int sz = sizeof(arr) / sizeof(arr[0]);
int ret=binary_search(arr, k, sz);
if (ret == -1)
{
printf("数组中未找到该数");
}
else {
printf("找到了,下标为%d", ret);
}
return 0;
}
void add(int* pn)
{
*pn += 1;
}
int main()
{
int num = 0;
add(&num);
printf("%d\n", num);
add(&num);
printf("%d\n", num);
add(&num);
printf("%d\n", num);
return 0;
}
函数和函数之间可以有机的组合的。
举例如下:
#include
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;
}
把一个函数的返回值作为另外一个函数的参数。
#include
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
//结果是啥?
return 0;
}
解释如下:我们print函数返回的是打印字符的个数,如果发生错误返回负数
我们又是从最里面的print开始,所以一开始先打印了43,
43一共2个字符,所以从里到外第二个print打印了2
2一共1个字符,所以最外面一个print打印了1
函数声明:
举个例子:
假如我把函数add写在main函数后面,因为代码是从上往下进行扫描的,那么我在读到add这行时,上面并没有add出现,就会报警告,如下图
那么函数声明就解决了这个问题
函数定义:
函数的定义是指函数的具体实现,交待函数的功能实现。
补充,我们后续写代码,一般函数声明放在.h文件中,函数定义放在同名的.c文件中,然后调用函数的文件只需要打开相应头文件即可。举个例子:
我现在这里有三个文件,add.h,add.c,源.c
分别放add函数的声明,add函数的定义,和源代码
如果想在源.c里面调用add函数,就不需要重写add函数了,
直接#include"add.h",然后就可以调用add函数了
程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应
用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复
杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可
描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在
于:把大事化小
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
递归练习题:
1.接受一个整型值(无符号),按照顺序打印它的每一位。 例如: 输入:1234,输出 1 2 3 4
void print(int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n%10);
}
int main()
{
int n = 0;
printf("请输入一个整数:");
scanf("%d", &n);
print(n);
return 0;
}
代码解释如下:
函数先跟随红色箭头往下“递”,再跟随蓝色箭头往上“归”
通过该题,我们也可以更好的感受到递归的两个重要条件
2.编写函数不允许创建临时变量,求字符串的长度。
我们先来看一下创建临时变量的方法(非递归)
//法1:非递归
//传的参数是数组名,数组名又是数组首元素的地址,我们这里用char*来接收参数
int my_strlen(char * x)
{
int count = 0;
while (*x != '\0')
{
count++;
x++;//指针向后挪一位,比如abc这个字符串,原先x指向a,x++后指向b
//ps:整形指针每次+1往后4字节(正好1整形)
//字符指针每次+1往后1字节(正好1字符)
//总之:每次指针+1都能指向后一个元素
}
return count;
}
int main()
{
//求字符串长度
char arr[10];
scanf("%s", arr);
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
上面非递归的方法虽然可以求出答案,但是题目要求不能创建临时变量,我们那个函数是创建了count这样的临时变量的。我们顺着上面非递归的思路,我们来写一下不创建临时变量(递归)的方法。
//法2:递归
int my_strlen(char * x)
{
if (*x != '\0')
{
x++;
return 1+my_strlen(x);//没有返回\0,也就是当前字符个数(1)+剩下字符个数
}
else //遇到\0了,也就说明递归已经找完了最后一个字符,这里返回0即可
{
return 0;
}
}
int main()
{
//求字符串长度
char arr[10];
scanf("%s", arr);
int len = my_strlen(arr);
printf("%d", len);
return 0;
}
例1:求n的阶乘。(不考虑溢出)
//设fac(n)=n!,fac(n)可以转化成一个分段函数
//fac(n)=1,n=1
//fac(n)=n*fac(n-1),n>1
int fac(int n)
{
if (n == 1)
return 1;
else
return n * fac(n - 1);
}
int main()
{
int a = 0;
printf("请输入一个数:");
scanf("%d", &a);
printf("%d的阶乘为%d", a,fac(a));
return 0;
}
例2:求第n个斐波那契数。(不考虑溢出)
int fib(int n)//求第n个斐波那契数
{
if (n == 1 || n == 2)
{
return 1;
}
else
{
return fib(n - 1) + fib(n - 2);
}
}
int main()
{
int n = 0;
printf("请输入一个数:");
scanf("%d", &n);
printf("第%d个斐波那契数为%d",n, fib(n));
return 0;
}
但是我们发现有问题:
在使用 fac 这个函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。
在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
我们上面写的求斐波那契第n个数,其实非常非常不适合递归,为什么?
比如我们要求第50个斐波那契数
按照递归的解法,我们要把50分成49 和48 ,再把49分成48 47 ,把48分成47和46…
第n层要计算2^n-1个数,当n=50时,要计算2的40多次方,这尼玛。。。
所以计算机表示:“wdnmd”,于是它就崩溃了
而且,由上图我们可以发现,上面很多计算都是重复的,
我们fib(47)就计算了三次,很多重复的数据还要重复计算干什么呢?
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: `stack overflow(栈溢出) 这样的信息。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
那如何解决上述的问题:
示例如下:
//求n的阶乘
int fac(int n)
{
int result = 1;
while (n > 1)
{
result *= n ;
n -= 1;
}
return result;
}
//求第n个斐波那契数
int fib(int n)
{
int result;
int pre_result;
int next_older_result;
result = pre_result = 1;
while (n > 2)
{
n -= 1;
next_older_result = pre_result;
pre_result = result;
result = pre_result + next_older_result;
}
return result;
}
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行开销。