【参考课程:B站 BV1cq4y1U7sg BV1Vm4y1r7jY】
//掌握函数的基本使用和递归
目录
[修改简化]: 对定义函数is_prime中的for循环,可以简化为:
[修改简化]:if 与 if else 可以通过逻辑操作符整合在一起
[修改简化]:if 与 if else 可以通过逻辑操作符整合在一起
【练习1】接受一个整型值(无符号),按照顺序打印它的每一位。
1.函数是什么
C语言中的函数--子程序[维基百科对函数的定义]
在计算机科学中,子程序(德语:unterprogramm,英语:subroutine, subprogram, callable unit),是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
函数在面向过程的语言中已经出现。是结构(struct)和类(class)的前身。本身就是对具有相关性语句的归类和对某过程的抽象。
2.库函数
*函数分类:库函数、自定义函数
//库函数位于各自的头文件中 <cstdio> (stdio.h)
常用库函数
- IO函数(输入输出函数) //printf scanf getchar putchar ...
- 字符串操作函数 //strcmp strlen ...
- 字符操作函数 //toupper ...
- 内存操作函数 //memcpy memcmp memset ...
- 时间/日期函数 //time ...
- 数学函数 //sqrt pow ...
- 其他库函数 //......
参照文档,学习库函数
E.g. strcpy
#include<stdio.h>
#include<string.h>
int main()
{
char arr1[20] = { 0 };
char arr2[] = "abc";
//使用strcpy函数将arr2中的内容拷贝到arr1中去
strcpy(arr1, arr2); //数组本身为地址(数组第一位的地址,所以此处不需要加指针)
//验证是否拷贝
printf("%s\n", arr1); //输出 abc --成功拷贝
printf("%s\n", arr2); //输出 abc --是拷贝而非移动
return 0;
}
3.自定义函数
//与库函数一样:有函数名、返回类型、参数
//自定义函数由我们自己设计 //建议设计函数功能要足够单一独立
//函数的组成
//返回类型 函数名(参数, ... )
ret_type fun_name(para1,para2)
{ //{}内为函数体
statement; //语句项
}
E.g.写一个函数能找出两个整数中的最大值
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//函数的定义
int get_max(int x, int y)
{
int z = 0;
if (x > y)
z = x;
else
z = y;
return z; //返回z--返回较大值 //z的类型即函数返回值类型
//return只能返回一个值
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a,& b);
//函数的调用
int max = get_max(a, b);
printf("max = %d\n", max);
return 0;
}
E.g.写一个函数用于交换两整型的值
#include<stdio.h>
//函数返回类型写成 void -表示函数不需要返回任何值
void swap(int x, int y)
{
int z = 0; //z作为x、y互相倒腾之间的临时中转站
z = x;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
printf("a = %d, b = %d\n", a, b); //交换前 //输出 a =10, b = 20
//函数的调用
swap(a, b);
printf("a = %d, b = %d\n", a, b); //交换后 //输出 a =10, b = 20
//说明代码有问题
return 0;
}
*代码出错,自定义函数swap没有交换a、b的值: //传值调用
原因:在主函数中定义的变量a、b开辟的储存地址并不是swap函数中参数x、y的储存地址
调用函数swap(a, b),只是将a的值赋给了x,把b的值赋给了y →在函数swap中x、y的值进行了交换,这并不会影响到主函数中的a、b →最终表现为自定义函数swap没有交换a、b的值
//swap在被调用的时候,实参传给形参--形参是实参的一份临时拷贝--改变形参不能改变实参
*解决方法: //传址调用
将swap函数的参数改为指针变量,这样相当于使用swap函数时直接在主函数定义的变量开辟的地址中进行值的交换,而不会重新开辟新的地址
修改后的代码:
#include<stdio.h>
//函数返回类型写成 void -表示函数不需要返回任何值
void swap(int*px, int*py)
{
int z = 0; //z作为x、y互相倒腾之间的临时中转站
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 20;
printf("a = %d, b = %d\n", a, b); //交换前 //输出 a =10, b = 20
//函数的调用
swap(&a, &b); //参数直接为指针变量
printf("a = %d, b = %d\n", a, b); //交换后 //输出 a =20, b = 10
return 0;
}
*上面举例的两个函数,get_max没有传函数地址,而swap则需要传地址。原因是因为get_max只需要返回一个值,不需要改变函数参数本身;而swap则需要更改函数参数自身的值,所以需要与调用函数时定义的参数建立联系(需要将函数内部与外部建立联系),以达到通过地址远程改变调用函数参数本身的目的。
4.函数参数
实际参数-实参
真实传递给函数的参数。(在函数调用时使用的参数)
//实参可以为常量、变量、表达式、函数等
无论实参是何种类型的量,在进行函数调用时它们都必须有确定的值,以便将这些值传送给形参
形式参数-形参
指在函数定义时括号中的变量。因为形式参数只有在函数被调用的过程中才会实例化(分配内存单元),所以叫形式参数。
形参在实例化之后其实相当于实参的一份临时拷贝
形参在函数调用完成之后就自动销毁了。因此形参只在函数中有效。
#include<stdio.h>
//函数的定义
void swap(int*px, int*py) //px、py为形式参数
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 20;
printf("a = %d, b = %d\n", a, b);
//函数的调用
swap(&a, &b); //&a、&b为实际参数
printf("a = %d, b = %d\n", a, b);
return 0;
}
5.函数调用
传值调用
//只需要借用函数参数值,不需要改变函数参数值时使用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参
传址调用
//需要改变函数参数值时使用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
//这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
【练习】
1.写一个函数可以判断一个数是不是素数
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//定义函数
int is_prime(int x) //采用【传值调用】
{
int n = 1;
//一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数→所以将m初始化为2
for (int m = 2; m < x; m++)
{
n = x % m;
if (0 == n)
break;
}
if (0 == n||x==0) //0不是素数
return 0;
else
return 1;
}
int main()
{
int a = 0;
printf("输入一个数判断是否为素数:");
scanf("%d", &a);
//调用函数
if (is_prime(a))
printf("%d是素数\n", a);
else
printf("%d不是素数\n", a);
return 0;
}
[修改简化]: 对定义函数is_prime中的for循环,可以简化为:
int is_prime(int x) //采用【传值调用】
{
int n = 1;
for (int m = 2; m < x||x==0; m++)
{
if (x %m == 0||x==0)
return 0; //直接返回0,不执行for循环外面的语句
}
return 1; //能到这一句的是走完for循环的数→一定是素数 ∴返回1
}
[修改优化]: 对素数的判断
x若为合数,则可写出:x=a*b ,a、b为x的因素
***a和b中一定至少有一个数字是<=开平方x的:
#include<math.h>
int is_prime(int x)
{
int n = 1;
//x若为合数,则可写出:x=a*b ,a、b为x的因素
//***a和b中一定至少有一个数字是<=开平方x的
//∴可以把“ m < x || x==0 ”优化为“ m <= sqrt(x) || x==0 ” //x需要试除的数变少了→由此优化了程序
//开平方x:sqrt(x) //sqrt -库函数,存储在头文件<math.h>中
for (int m = 2; m <= sqrt(x) || x==0; m++)
{
if (x %m == 0||x==0)
return 0;
}
return 1;
}
2.写一个函数判断一年是不是闰年
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//普通闰年:公历年份是4的倍数且不是100的倍数为普通闰年(如2004、2020年就是闰年)。
//世纪闰年:公历年份是整百数的,必须是400的倍数才是世界闰年(如2000是世纪闰年,1900不是世纪闰年)。
//【总结】能被4整除却不能被100整除,或能被400整除的年份就是闰年!
int is_leap_year(int year)
{
if (year % 400 == 0)
return 1;
else if (year % 4 == 0 && year % 100 != 0)
return 1;
else
return 0;
}
int main()
{
int y = 0;
printf("请输入年份:");
scanf("%d", &y);
//调用函数
if (is_leap_year(y)) //【传值调用】
printf("%d是闰年", y);
else
printf("%d不是闰年");
return 0;
}
[修改简化]:if 与 if else 可以通过逻辑操作符整合在一起
int is_leap_year(int year)
{
if ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0))
return 1;
else
return 0;
}
[修改简化]:if 与 if else 可以通过逻辑操作符整合在一起
int is_leap_year(int year)
{
return ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0));
//为真→返回1;为假→返回0
}
3.写一个函数,实现一个整型有序数组的*二分查找*
在数组{0,2,3,6,7,8,12,14,21,37,40}中查找7:
#include<stdio.h>
//定义函数
//形参arr[]要带[]--为了告诉计算机这个参数是一个数组。若不写[],计算机认为arr是一个整型数
//***与实参区分 (∵在形参前并没有任何定义告诉计算机arr是数组)
int binary_search(int arr[], int key, int size)
{
int left = 0;
int right = size-1;
while (left <= right)
{
int mid = (left + right) / 2;
if (key < arr[mid])
{
right = mid - 1;
}
else if (key > arr[mid])
{
left = mid + 1;
}
else
return mid;
}
return -1;
}
int main()
{
//数组与要查找的数
int a[] = { 0,2,3,6,7,8,12,14,21,37,40 };
int k = 7;
//数组相关参数
int sz = sizeof(a) / sizeof(a[0]); //求数组元素个数
//调用自定义函数binary_search
//找到就返回元素下标
//找不到返回-1 ∵在C语言数组中元素下表不可能为-1(区别于python)
int ret = binary_search(a, k, sz);
//此处实参a在前面已经明确定义为数组,不再加[]→***与形参的差别
//a[]是错误的格式
if (-1 == ret)
printf("找不到元素\n");
else
printf("找到元素,下标为%d\n", ret);
return 0;
}
*数组传参
传递的不是数组本身,实际仅仅传过去了数组首元素的地址
所以:
int binary_search(int arr[], int key, int size)
应写为:
int binary_search(int* arr, int key, int size)
∴数组元素个数只能在函数外求,没办法在函数内求
4.写一个函数,每调用一次这个函数,就会将num的值增加1
#include<stdio.h>
void ADD(int* p)
{
(*p)++; //注意*p要括起来表示为一个整体--解引用地址p
}
int main()
{
int num = 0;
ADD(&num); //函数需要改变参数的值∴采用【传址调用】
//ADD函数参数类型为地址,所以要用取地址操作符&取num地址
//验证
printf("%d\n", num); //输出 1
ADD(&num);
printf("%d\n", num); //输出 2
ADD(&num);
printf("%d\n", num); //输出 3
return 0;
}
6.函数的嵌套调用和链式访问
嵌套调用
**函数不能嵌套定义--每一个函数都应该在{}外独立存在--函数之间是平等的
但函数可以嵌套调用
链式访问
把一个函数的返回值作为另一个函数的参数
#include<string.h> //为调用库函数strlen
#include<stdio.h>
int main()
{
//非链式访问
int len = strlen("abc");
printf("%d\n", len); //输出 3
//链式访问
printf("%d\n", strlen("abc")); //与上面结果一致,输出 3
return 0;
}
*printf的返回值:
如果成功,则返回写入的字符总数。
如果出现写错误,则设置错误指示器(error)并返回一个负数。
如果在写宽字符时出现多字节字符编码错误,则errno被设置为EILSEQ并返回一个负数。
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43))); //输出 4321
return 0;
}
//最内层printf首先打印43
//次内层printf打印其返回值2(‘43’字符数为2)
//最外层打印次内层返回值1(‘2’字符数为1)
∴最终打印 4321
printf("%d\n", printf("%d\n", printf("aaa%d\n", 43)));
*值得注意的是:\n也占1个字符数
7.函数的声明和定义
//函数的声明--告知函数的存在
//函数的定义--创建函数--定义是一种更强有力的声明
我们在main函数后创建一个ADD函数为例:
#include<stdio.h>
int main()
{
int a = 3;
int b = 7;
printf("%d", ADD(a, b));
return 0;
}
int ADD(int x, int y)
{
return x + y;
}
∴为了消除警告,需要在main函数前声明ADD函数的存在:
#include<stdio.h>
int main()
{
int a = 3;
int b = 7;
//声明函数:
//只需要模糊告知即可--函数的返回类型、函数名、参数类型
int ADD(int, int);
printf("%d", ADD(a, b));
return 0;
}
int ADD(int x, int y)
{
return x + y;
}
函数的声明一般出现在函数使用之前,要满足先声明后使用
*函数的声明一般放在头文件中 //头文件发声明,源文件写程序 //*静态库(lib)
8.函数递归
程序调用自身的编程技巧称为递归--递归的主要思想:大事化小
#include<stdio.h>
int main()
{
printf("二狗\n");
main();
return 0;
}
以上程序在输出大量二狗后被强制退出报错:
栈溢出Stack overflow原因:递归层次太深,即函数调用过多
递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续
- 每次递归调用之后越来越接近这个条件
#必要非充分--满足以上两个条件的函数递归也可能报错:
#include<stdio.h>
void test(int num)
{
if (num < 10000)
{
test(num + 1);
}
}
int main()
{
test(1);
return 0;
}
程序运行后报错: 栈溢出
#补充:写递归代码的时候
- 首先要满足两个必要条件,即:不能死递归,要有每次递归都能逼近的递归条件
- 递归层次不能太深
【练习1】接受一个整型值(无符号),按照顺序打印它的每一位。
//例如:输入 1234,输出 1 2 3 4
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void print(unsigned int n) //unsigned int -无符号整型
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf("%u", &num); //%u -无符号整型占位符
print(num); //创建自定义函数 print 完成题目要求
return 0;
}
【练习2】编写函数不允许创建临时变量,求字符串的长度
思路:创建自定义函数模仿 strlen函数 的功能:
#include<string.h> #include<stdio.h> int main() { char arr[] = "met"; //['m']['e']['t']['\0'] printf("%d\n", strlen(arr)); //输出 3 //不把 结束符['\0'] 计算入字符串长度中 return 0; }
#include<string.h>
#include<stdio.h>
//参考strlen返回值为数组长度--即整型 ∴my_strlen函数返回值我们也设定为整型
//参考strlen参数为数组名--数组名实际传递的是数组第一个元素的地址 ∴my_strlen函数参数为指针变量类型
int my_strlen(char* str)
{
if (*str != '\0')
return 1 + my_strlen(str+1); //注意此处使用str+1 而非++str或str++
//++str/str++ 会将str自身值改变,在递归中带来副作用
else
return 0;
}
int main()
{
char arr[] = "met";
//['m']['e']['t']['\0']
printf("%d\n", my_strlen(arr));
return 0;
}
【练习3】求n的阶层
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int n = 0;
printf("n=");
scanf("%d", &n);
int Fac(int); //函数的声明
printf("n!=%d\n", Fac(n));
return 0;
}
int Fac(int num)
{
if (num > 0)
return num*(Fac(num - 1));
else
return 1; //补充数学基础知识:0!=1
}
【练习4】(递归)求第n个斐波那契数(不考虑溢出)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int n = 0;
printf("n=");
scanf("%d", &n);
int Fib(int);
printf("第%d个斐波那契数是%d", n, Fib(n));
return 0;
}
int Fib(int num)
{
if (num >= 3)
return (Fib(num - 1) + Fib(num - 2));
else
return 1;
}
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> //定义一个全局变量来统计递归计算斐波那契数所需要的次数 int count = 0; int main() { int n = 0; printf("n="); scanf("%d", &n); int Fib(int); printf("第%d个斐波那契数是%d\n", n, Fib(n)); printf("计算次数为:%d\n", count); return 0; } int Fib(int num) { //由于采用递归法,每计算一个斐波那契数(num>=3)都会经历num==3这一层递归 //所以通过统计第三个斐波那契数的计算次数来反映计算一个斐波那契数需要的计算量 if (num == 3) count++; if (num >= 3) return (Fib(num - 1) + Fib(num - 2)); else return 1; }
结果:
说明:若计算第40个斐波那契数--以上程序可行,但效率太低 ∵进行了大量重复的计算
递归与迭代
递归(recursion):递归常被用来描述以自相似方法重复事物的过程,在数学和计算机科学中,指的是在函数定义中使用函数自身的方法。(A调用A)
迭代(iteration):重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。(A重复调用B)
递归是一个树结构,从字面可以其理解为重复“递推”和“回归”的过程,当“递推”到达底部时就会开始“回归”,其过程相当于树的深度优先遍历。
迭代是一个环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。
【练习4-优化】(迭代)求第n个斐波那契数(不考虑溢出)
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{
int n = 0;
printf("n=");
scanf("%d", &n);
int Fib(int);
printf("第%d个斐波那契数是%d\n", n, Fib(n));
return 0;
}
int Fib(int num)
{
//m -循环次数,fm -第m个斐波那契数,fm1 -第(m-2)个斐波那契数,fm2 -第(m-1)个斐波那契数
int m = 0;
int fm1,fm2,fm = 1;
if (num < 3)
return 1;
else
{
for (m = 3, fm1 = 1, fm2 = 1; m <= num; m++, fm1 = fm2, fm2 = fm)
{
fm = fm1 + fm2;
}
return fm;
}
}
//当程序使用递归 -容易理解且无明显缺陷时,采用递归
当有明显缺陷时(溢出/效率低下 ......),采用迭代(非递归的方式)优化代码