思维导图:
1. 函数是什么?
函数就是子程序,就是一个大型程序中的部分代码,并具有相对独立性。
2. C语言中函数的分类:
2.1 库函数:
标准库-提供了C语言相关的库函数
那为什么要有库函数呢?
在编程的过程中,我们总是会用到printf去打印一个数;
或者是经常会用到scanf去输入一些值。
拥有了库函数,程序员就不用每次使用这些值得时候去写程序,直接调用库函数就行,
这样就大大提高了编写代码的效率。
2.1.1 如何学会使用库函数?
cplusplus.com - The C++ Resources Network
这里推荐一个可以学习C语言库函数的网站。
简单总结,C语言常用的库函数:
我们参照网站的文档,可以学习库函数。
这是库string.h中的函数。
我们可以看到它的返回类型是char*类型的指针,函数名是strcpy,需要传两个char*类型的参数。
通过它的描述,我们可以得知这个函数是将字符source拷贝到‘目的地’destination,包括‘\0’字符。
这是对两个参数的具体解释。
函数的返回值是destination这个字符。
这样我们就大致了解了这个函数,可以进行简单的应用了。
#define _CRT_SECURE_NO_WARNINGS 1 #include #include//引用该函数头文件 int main() { char arr1[20] = { 0 }; char arr2[] = "welcome to my blog!";//创建两个字符数组 strcpy(arr1, arr2); //数组名代表的是数组首元素的地址 printf("%s",arr1); //我们运用函数将arr2的值拷贝到arr1中 return 0; }
打印出来的结果是:
welcome to my blog!
当然,这并不代表我们已经精通这个函数了,重要的是多加练习。
2.2 自定义函数
不过,比库函数更重要的是自定义函数,顾名思义,这需要我们自己来设计函数。
比如说:我想设计一个函数找出两个整数的最大值:
//设计思路 //输入两个整数 //设计一个函数实现目标 #include int get_max(int x, int y)//返回类型是int,而int x接收a的值,int y接收b的值 { return (x > y ? x : y);//函数的返回值 } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); int max = get_max(a, b);//创建一个整形变量接收函数的返回值 printf("%d\n", max); //函数将a和b传值 return 0; }
输出的结果是:
输入:10 20 输出:20
这就是一个函数的基本形式。
再举一个例子:
设计一个函数交换两个整形变量的值:
#include void exchange(int x, int y)//void表示函数没有返回类型,所以不用return。 { int tmp = x; x = y; y = tmp; } //交换两个值 int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); exchange(a, b); printf("a = %d b = %d", a, b); return 0; }
但是输出的结果是:
输入:3 5 输出:a = 3 b = 5
我们发现它并a和b没有交换各自的值,究竟是为什么呢?
这就要说到函数的参数。
3. 函数的参数
3.1 实际参数(实参):
我们传给函数的参数是实参;实参可以是:常量、变量、表达式、函数等。
但实参一定要是一个确切的数。
3.2 形式参数(形参):
形参是指函数名后面,接收我们传过去的参数的参数。
形式参数当函数调用完成之后就会自动销毁,类似于局部变量,形参只在函数内生效。
而且形参在创建时会拥有自己的内存空间,并拥有实参的内容,这也是上文出错的原因,
总结:所以形参可以被看成是实参的一份临时拷贝。
那我们该如何实现上文的那个函数呢?
这就要说到函数的调用规则了:
4. 函数的调用:
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2 传址调用
这种传参方式可以让函数和函数外边的变量建立起真正的联系,
也就是函数内部可以直接操作函数外部的变量,也就是实现所谓的远程操控。
//怎样才能让函数实现a,b值之间的交换呢? #include void exchange(int* pa, int* pb)//形参//创建指针进行接收 { //这样形参就能通过地址远程操控主函数中的变量 int tmp = *pa;//tmp = a //*解引用 *pa = *pb; //a = b *pb = tmp; //b = tmp } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); exchange(&a, &b);//实参//我们可以将a和b的地址传给函数 printf("a = %d b = %d", a, b); return 0; }
输出的结果:
输入:3 5
输出:a = 5 b = 3
这样就能成功交换a和b的值了。
4.3 练习
趁热打铁,练习是巩固新知的最好方法。
1. 写一个函数判断一个数是不是素数:
#include #include//函数sqrt的调用 int is_prime(int n) { int j = 0; for (j = 2; j <= sqrt(n); j++)//sqrt是开平方函数:根号n { //试除法判断素数,运用sqrt函数提高代码效率 if (n % j == 0) { return 0;//不是素数 } } return 1;//是素数 } int main() { int i = 0; scanf("%d", &i); if (is_prime(i)) { printf("是素数"); } else { printf("不是素数"); } return 0; }
输出结果:
输入:100 输出:不是素数 输入:101 输出:是素数
2. 写一个函数判断一年是不是闰年:
#include int is_leap_year(int x) { return (x % 4 == 0 && x % 100 != 0 || x % 400 == 0); } //&&,||这两个符号返回的值就是0和1,可以直接返回 int main() { int i = 0; scanf("%d", &i); if (is_leap_year(i)) { printf("是闰年"); } else { printf("不是闰年"); } return 0; }
输出结果:
输入:1000 输出:不是闰年 输入:2000 输出:是闰年
3. 写一个函数,实现一个整形有序数组的二分查找:
//代码思路: //创建一个整形有序数组 //设置输入 //创建函数实现二分查找 #include int binary_search(char* arr, int sz, int n) { //arr数组名代表数组首元素地址,用指针接收 int left = 0; int right = sz - 1; int mid = 0; while (left <= right) { mid= (left + right) / 2; if (arr[mid] > n) { right = mid - 1; } else if (arr[mid] < n) { left = mid + 1; } else { return mid; } } return 0; } int main() { char arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int n = 0; scanf("%d", &n); int sz = sizeof(arr) / sizeof(arr[0]); if (binary_search(arr, sz, n)) { printf("找到了,下标是:%d", binary_search(arr, sz, n)); } else { printf("找不到"); } return 0; }
输出结果:
输入:7 输出:找到了,下标是6 输入:0 输出:找不到
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1
//代码思路 //通过传址调用,在调用函数时让num++ #include void add_num(int* num) { (*num)++; return *num; } int main() { int num = 0; add_num(&num); printf("%d\n", num); add_num(&num); printf("%d\n", num); add_num(&num); printf("%d\n", num); return 0; }
输出结果:
输出: 1 2 3
5. 函数的嵌套调用和链式访问、
函数与函数之间是可以互相调用的。
5.1 嵌套调用
例:
#include void print() { printf("hehe\n"); } void circulate() { int i = 0; for (i = 0; i < 3; i++) { print();//函数circulate调用函数print } //这个就是嵌套调用 } int main() { circulate(); return 0; }
输出结果:
输出: hehe hehe hehe
注:函数可以嵌套调用但是不能嵌套定义。
5.2 链式访问
例:
#include int main() { printf("%d", printf("%d", printf("%d", 43))); return 0; //这就是一种链式访问 }
/
你们可以猜猜输出的结果是什么?
输出结果:
输出:4321
为什么会出现这种情况呢?
其实,在不同的编译器会有不同的结果,在VS2019这个编译器中
printf函数的返回值是打印在屏幕上的字符的个数。
6. 函数的声明和定义
6.1 函数声明:
1. 函数的声明就是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。
2. 函数的声明一般出现在函数的使用之前,函数需要先声明再使用。
3. 函数的声明一般放在头文件。
例:
#include int Add(int x, int y);//这是一个函数的声明 //编译器自上而下扫描代码,如果函数的定义在调用之后,需要先声明 int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("%d\n", Add(a, b));//链式访问的应用 return 0; } int Add(int x, int y)//一个简单的求和函数 { return x + y;
输出结果:
输入:3 5 输出:8
6.2 函数定义:
函数的定义是指函数的具体实现。
例:
#include int Add(int x, int y)//函数的定义 { //函数的定义也是一种特殊的声明 return x + y; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("%d\n", Add(a, b));//链式访问的应用 return 0; }
输出结果:
输入:3 5 输出:8 拓展:
变量也遵循类似的规则:
int g_val;//变量的声明 int main() { printf("g_val = %d\n", g_val); return 0; } int g_val = 2022;//变量的定义
输出结果:
输出:2022
以上是函数定义的基本规则,那函数定义与声明的应用场景是什么呢?
我们将主函数放在11_1.c源文件中。(这是我的测试模块)
引了头文件后我们就能正常使用函数了 。
将函数的实现放在add.c源文件中,函数定义放在add.h头文件中。(加法模块)
这样做有些什么好处呢?
1. 可以实现模块化开发,也就是分工:
在一个大的项目中,例如实现一个计算器,可以一个程序员写加法,另一个写减法,提高效率。
2. 可以实现代码的隐藏。
7. 函数递归
7.1 什么是递归
程序调用自身的编程技巧称为递归。(即函数自己调用自己)
递归能将一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
只需少量的程序就可描述出解题过程所需要的多次重复计算,从而大大地减少程序的代码量,
递归的主要思考方式在于:把大事化小。
例:
#include int main() { printf("hehe"); main();//main函数自己调用自己 return 0;//这就是一种递归 }
输出结果:
死循环打印hehe
这是一种错误的递归。
这段代码打印到最后,程序会挂掉:
编译器提醒:Stack overflow(意思是:栈溢出)
那什么是栈溢出呢?
我曾在初识C语言中提到过内存的三个分区:
然后程序就挂了。
那怎样才能写出一个正确的递归呢?
7.2 递归的两个必要条件
1. 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2. 每次递归调用之后越来越接近这个限制条件。
我们通过练习来感受递归是怎么使用的。
7.2.1 练习1:
接受一个无符号整形,按照顺序打印它的每一位:
例如:输入:1234 打印:1 2 3 4
#include void print(int n) { if (n > 9) { print(n / 10); } printf("%d ", n % 10); } int main() { int n = 0; scanf("%d", &n); print(n); return 0; }
输出结果:
输入:4257
输出:4 2 5 7
分析:
7.2.2 练习2:
编写函数求字符串的长度:(模拟实现strlen函数)
#include #include int my_strlen(char* str) { int count = 0; while (*str != '\0') { count++; str++;//字符指针+1,向后跳1个字符 } return count; } int main() { char arr[] = "cat"; //[c a t \0] //数组名其实是数组首元素的地址 int len = my_strlen(arr); printf("%d\n", len); return 0; }
输出结果:
输出:3
这个代码通过创建临时变量count,记录字符数。
如果不允许创建临时变量,又该怎么做呢?
#include int my_strlen(char* str) { if (*str != '\0') { return 1 + my_strlen(str + 1);//(str+1)使得下标每次调用右移一位 } //每次调用函数也会使函数的返回值加一 else { return 0; } } int main() { char arr[] = "cat"; int len = my_strlen(arr); printf("%d", len); return 0; }
输出结果:
输出:3
其实这个代码的原理与练习1的是相同的,这就是函数递归的思考方式与应用。
7.3 递归与迭代
有些时候,我们遇到的问题可能递归并不好用了
7.3.1 练习3:
求n的阶乘:
#include int fan(int n) { if (n <= 1) { return 1; } else { return n * fan(n - 1); } } int main() { int n = 0; scanf("%d", &n); int ret = fan(n); printf("%d", ret); return 0; }
输出结果:
输入:5
输出:120
(弊端)但是,如果要计算的值太大,函数调用太多,会导致栈溢出。
7.3.2 练习4:
求第n个斐波那契数:
#include int fib(int n) { if (n <= 2) { return 1; } else { return fib(n - 1) + fib(n - 2); } //斐波那契数:1,1,2,3,5,8,13,21,34,55...... } int main() { int n = 0; scanf("%d", &n); int ret = fib(n); printf("%d", ret); return 0; }
输出结果:
输入:10
输出:55
输入:60
输出: //程序一直在跑,输出结果迟迟未出
为什么会出现这样的结果呢?
因为使用递归来求效率太低了。
因此,我们可以用迭代来求,其实迭代可以简单理解成除递归以外的方法(循环)
#include 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; }
输出结果:
输入:60
输出:1820529360
//这次一瞬间就打印出来了
总结:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 许多问题的迭代实现比递归实现效率更高,但是递归的形式更容易理解。
3. 如果问题太复杂,用迭代实现代码量太多,用递归实现会更简洁(补偿代码效率低的缺点)。
写在最后:
以上就是本篇文章的内容了,感谢你的阅读。
如果喜欢本文的话,欢迎点赞和评论,写下你的见解。
如果想和我一起学习编程,不妨点个关注,我们一起学习,一同成长。