前言
⭐️此篇博文主要带大家探讨C语言的函数部分的知识点,若有错误,还请佬指出,一定感谢!
制作不易,若觉得内容不错可以点赞+收藏❤️,这是对博主最大的认可!
函数的定义
C语言的函数与数学中的函数术语有啥区别?
数学中的函数可以理解为输入自变量,输出应变量;计算机编程中的函数通常被定义为一个具有名称、参数、返回值、访问修饰符等属性的代码块(block)。
为啥编程语言有函数这种概念?(这里指计算机编程中,以下提到的函数无特殊说明都指计算机编程中的函数)
函数可以帮助程序员将代码进行模块化,提高代码的可读性和可维护性。将一个能实现某个功能的代码块(block)打包为函数,以调用的方式来实现这个代码块(block)所能实现的功能,能有很多优势。
函数与API(Application Programming Interface)——应用程序编程接口的关系。
函数的某些特点与API接口的特点相似,故可将函数看做C语言中的API接口之一。
计算机中专业性词语较多,感兴趣的同学可以百度找查资料。
函数的分类
库函数
库函数是啥?为什么要用库函数?
C语言的库函数是指一组可重用的函数,它们已经被写好并编译成库文件,可以被其他程序调用和使用。C语言提供了许多标准库函数,如stdio.h、stdlib.h、math.h等,用于各种常见的操作,如输入输出、字符串处理、数学运算等。此外,还有许多其他的库函数,如graphics.h、conio.h等,用于图形界面和控制台操作。在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
How to learn?
我推荐www.cplusplus.com这个网址。
常见的一些库函数
1️⃣IO函数(输入输出函数)(头文件为stdio.h)
常见的有scanf函数和printf函数。在我的初识C语言(1)里讲解过格式化输入输出函数,链接如下:初识C语言(1)-CSDN博客
2️⃣字符串操作函数(头文件为string.h)
✏️strcpy函数(字符串拷贝函数)
strcpychar* strcpy (char *destination, const char *source);
函数讲解
函数名strcpy
函数的参数
1.char *destination
待粘贴空间的起始地址
2.const char *source
被拷贝空间的起始地址
函数的返回值
char *
返回destination最初指向空间的起始地址
举例:
#include #include int main(){char arr1[4];strcpy(arr1, "abc");//返回arr1的地址printf("%s\n", arr1);//所以也可以将arr1替换为strcpy(arr1, "abc")char arr2[4];char arr3[4] = "abc";strcpy(arr2, arr3);//返回arr2的地址printf("%s\n", arr2);char arr4[5];char arr5[5] = {'a','b','c','\0','d'};strcpy(arr4, arr5);//返回arr4的地址printf("%s\n", arr4);char arr6[4];char arr7[3] = { 'a','b','c' };strcpy(arr6, arr7);//返回arr6的地址printf("%s\n", arr6);return 0;}
☑️(大部分情况数组名就是数组首元素的地址)在arr1和arr2的拷贝中,可见strcpy拷贝的是整个字符串的(包括\0);在arr4的拷贝中,可见strcpy拷贝的是\0之前(包括\0)的所有字符;arr6的拷贝由于没有拷贝\0到arr6中,所以打印字符串arr6时出错。(%s格式打印是打印字符串或者字符数组中\0之前的字符,它是以\0为结束标志的)
✏️memset函数
memsetvoid *memset (void *ptr, int value, size_t num)
函数讲解
函数名memset
函数的参数
1.void *ptr
指针指向的要填充的那个内存块的首地址
2.int vaule
要设置的值
3.size_t num
要设置的字节数
函数的返回值
void *
返回被设置好的ptr指针指向空间的地址
举例:
#include #include int main(){char arr1[5] = "abcd";memset(arr1, 'a', 3);//返回arr1的地址printf("%s\n", arr1)//可以替换为memset(arr1, 'a', 3)return 0;}
思考为啥第二个参数的类型是int,可以是char吗?
自定义函数
既然C语言提供了基础的库函数为啥还要支持自定义函数?
因为自定义函数是我们自己来设计,这给程序员一个很大的发挥空间。自定义函数和库函数一样,也有函数名、参数、返回值。
ret_type fun_name(para1, ...){ statement;//语句项}
函数的基本组成
fun_name 函数名
para1 函数参数 statement函数体
ret_type 返回类型
举例:写一个函数可以找出两个整数中的最大值。
思路:
函数名(自定义):Get_Max
函数参数:int x、int y(不一定有,没有参数时空出来,函数参数记得带类型)
函数体:…
返回值(返回类型):int(也可以无返回值,写为void)
实现:
int Get_Max(int x, int y){if (x > y)return x;elsereturn y;}
注:定义一个新的变量来接收该函数的返回值,也可以直接将printf中的ret换成Get_Max(3, 5)。
举例
#include void Print(){printf("abc");}int main(){Print();return 0;}
举例:写一个函数可以交换两个整形变量的内容。
思路:
函数名:swap
函数参数:int x, int y(看实际需求要不要参数)
函数体:…
返回值(返回类型):void(这里不需要返回啥)
实现:
#include void swap(int a, int b){int temp = b;b = a;a = temp;}int main(){int x = 3;int y = 5;swap(x, y);printf("%d %d\n", x, y);return 0;}
思考若交换的部分改为a= b; b= a会发生啥?为啥上述交换任然失败了?
直接交换会导致值的覆盖,至于上述交换为啥失败了,看下图:
☑️main函数里定义的两个变量有自己的地址(空间),swap函数中的a,b也有自己独立的空间,所以上述操作就相当于更换了a和b的值,并没有改变x、y的值。
改进:
#include void swap(int *a, int *b){int temp = *a;*a = *b;*b = temp;}int main(){int x = 3;int y = 5;swap(&x, &y);printf("%d %d\n", x, y);return 0;}
☑️利用指针变量来存储变量的地址,通过解引用(*)来找到变量对应的空间,以此来达到从一个空间操作另一个空间。这里a指针变量存储着x的地址,通过*a来找到x那块空间以此来修改x空间对应的值。这样的操作叫做传址操作;最上面的叫做传值操作,传值只是一份临时拷贝,无法从一个空间操作另一个空间。
函数的参数
函数的实际参数
真实传给函数的参数,叫实参。比如上述例子的3,5,x,y,&x,&y…
实参可以是:常量、变量、表达式、函数等。
☑️无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。形参和实参可以同名。
函数的形式参数
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
☑️形参只是实参的一份临时拷贝!函数调用结束,形参的生命周期也就结束了。
函数调用
啥是函数调用?
函数的调用指的是在程序中通过函数名和参数列表来执行函数代码的过程。在C语言中,调用函数需要使用函数名和一对括号“()”,并将实参列表放在括号内。
函数的传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
函数的传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式,这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。(理论上传址传递的也是地址的值) 上述举例2:写一个函数可以交换两个整形变量的内容,已经说明了该内容,不再赘余。
思考下述代码是哪种调用?
void swap(int* a, int* b){//...}int main(){int* p1;int* p2;swap(p1, p2);return 0;}
练习:
1.写一个函数可以判断一个数是不是素数。
思路无非就是函数体内部怎么实现
素数的判断:只能被1和自己整除的数
整个循环,让循环变量i从1开始到num(设num为传进来的参数),判断num是否能整除i。
有能整除的数就说明不是素数,则跳出循环(break),是素数则要走完整个循环还没有能整除的数。
#include void Is_prime(int num){int i = 0;if (num == 1)//特殊情况{printf("不是素数\n");return;}else if (num == 2){printf("这是最小的素数\n");return;}int n = 0;//作为判断是素数还是不是素数的状态for (i = 2; i < num; i++){if ((num % i) == 0)//能被1~num之间的其他数整除{n = 1;//做个标记,表示是遇到了能整除的数而跳出的循环break;}}if (n == 1)printf("不是素数\n");elseprintf("是素数\n");}int main(){int x = 0;printf("请输入一个整数:");scanf("%d", &x);Is_prime(x);return 0;}
改进1:
#include int Is_prime(int num){if (num == 1)return 1;//特殊情况先安排int i = 0;for (i = 2; i < num; i++){if ((num % i) == 0)return 1;//有能被其他整除的数}return 0;}int main(){int x = 0;printf("请输入一个整数:");scanf("%d", &x);int n = Is_prime(x);if (n == 1)printf("不是素数\n");elseprintf("是素数\n");return 0;}
改进2:
C语言里还有一种布尔类型
C99中引入的
bool类型的变量只有两种取值true和false
#include #include bool Is_prime(int num)//记得引入头文件,bool也可写为_Bool,true的值是1,false的值是0{if (num == 1)return true;//特殊情况先安排int i = 0;for (i = 2; i < num; i++){if ((num % i) == 0)return true;//有能被其他整除的数}return false;}int main(){int x = 0;printf("请输入一个整数:");scanf("%d", &x);int n = Is_prime(x);if (n == 1)printf("不是素数\n");elseprintf("是素数\n");return 0;}
☑️根据适合场景选择是否要有返回值,是传值还是传址。
思考如何输出1~200中是素数的数呢?
2.写一个函数判断一年是不是闰年。
思路:
闰年的判断为能被400整除或者能被4整除且不能被100整除
#include #include bool Is_Leapyear(int Year){if ((Year % 4 == 0 && Year % 100 != 0) || (Year % 400 == 0))return true;elsereturn false;}int main(){int year;printf("请输入年份:");scanf("%d", &year);int ret = Is_Leapyear(year);if (ret == 1)printf("是润年\n");elseprintf("不是闰年\n");return 0;}
思考如何输出1000~2000年之间哪些是闰年,并统计出有多少个闰年。
3.写一个函数,实现一个整形有序数组的二分查找。
实现:
void binary_search(int arr[], int k, int sz){int left = 0;int right = sz - 1;while (left arr[mid]){left = mid + 1;}else if (k right)printf("没找到\n");}int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int k;printf("请输入你要查找的数字:");scanf("%d", &k);int sz = sizeof(arr) / sizeof(int);binary_search(arr, k, sz);return 0;}
注:二分查找(折半查找)的思路如下
若left right),没找到。
思考求平均值mid = (left + right) / 2″ />有的,因为left和right是int类型,left+right的范围如果大于int可以表示的最大范围了,则会有数据溢出。
改为:
mid = (right - left) / 2 + left;
思路:
4.写一个函数,每调用一次这个函数,就会将 num 的值增加1。
实现:
#include void Print(int *p){printf("Hehe\n");(*p)++;}int main(){int num = 0;Print(&num);printf("第%d次调用\n", num);Print(&num);printf("第%d次调用\n", num);Print(&num);printf("第%d次调用\n", num);return 0;}
函数的嵌套调用与链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
函数的嵌套调用
举例
#include void Print(){printf("Hello world!\n");}void test(){int i = 0;for (i = 0; i < 2; i++){Print();}}int main(){test();return 0;}
举例:嵌套定义
#include void test(){void Print(){printf("Hello world!\n");}}int main(){test();return 0;}
☑️函数可以嵌套调用但是不能嵌套定义
函数的链式访问
函数的链式访问是指:把一个函数的返回值作为另外一个函数的参数。
举例
#include int main(){char arr[10];printf("%d\n", strlen(strcpy(arr, "abc")));return 0;}
举例
#include int main(){printf("%d", printf("%d", printf("43")));return 0;}
思考会输出啥?
先来看printf的返回值是啥?
☑️在Cplusplus官网上查找到printf的返回类型是int类型,返回值是打印在屏幕上字符的个数,所以printf(“43”)先打印43,再返回2作为参数被前面那个printf接收…
若题目改为如下:
#include int main(){printf("%d\n", printf("%d\n", printf("43\n")));return 0;}
思考又会输出啥?
函数的声明和定义
函数的声明
1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。 2.函数的声明一般出现在函数的使用之前。要满足 先声明后使用。 3.函数的声明一般要放在头文件中。
举例:写一个函数,完成两个数的相加
#include int main(){int num1;int num2;scanf("%d %d", &num1, &num2);int ret = Add(num1, num2);//函数的调用printf("%d\n", ret);return 0;}int Add(int x, int y)//函数的定义{return x + y;}
这就是一个典型的先调用无声明例子,但是我的VS2022居然可以跑起来且不报错…很离谱嗷。
改进:
#include //int Add(int x, int y);//可以在这里声明int main(){int num1;int num2;scanf("%d %d", &num1, &num2);int Add(int x, int y);//也可以在这里声明,总之要先声明后调用!int ret = Add(num1, num2);//函数的调用printf("%d\n", ret);return 0;}int Add(int x, int y)//函数的定义{return x + y;}
☑️函数的声明是由编译器检查的,而函数的定义(实现)是由链接是检查的,有了函数的声明但无函数的定义(实现)在编译阶段是不会报错的,但是在链接时会报错;虽然函数不能嵌套定义,但是可以在函数的内部声明另一个函数(上述例子中main函数中就声明了Add函数)。
函数的定义
1.函数的定义是指函数的具体实现,交待函数的功能实现。
2.函数的定义也是一种特殊的声明!
3.函数不能嵌套定义
又如上述例题可改进为:
#include int Add(int x, int y){return x + y;}int main(){int num1;int num2;scanf("%d %d", &num1, &num2);int ret = Add(num1, num2);printf("%d\n", ret);return 0;}
举例:工程上,函数的声明一般要放在头文件中;函数的定义(实现)放在.c文件中
1.主函数一般用来写思路,调用其他函数来实现某种功能,记得包含你要使用的Add.h头文件
2.对应的头文件用来进行对函数的声明
3.对应的.c文件用来进行对函数的实现
4.最后的效果
☑️在主函数里,#include “Add.h”就等价于把Add.h里声明的函数(这里是int Add(int x, int y))拿过来拷贝一份,然后编译能通过了,在同一个工程中,又因为函数具有外部链接属性,所以链接也能通过。Add.c和test_10_26.c都会被编译器编译,若无问题,再链接。
举例:代码的保护之导入静态库
1.新建一个项目
2.更改项目属性
3.编译
4.找到生成的.lib文件
5.把这个.lib(别人看不到你的代码是如何实现的,记事本打开里面是乱码)和Add.h文件给别人使用,这里我给另一个项目使用
6.在另一个项目中添加这两个文件
7.把.h文件拖动到头文件里
8.导入静态库
注:如果出现这种情况
请将x86环境改为x64环境
函数的递归
什么是函数的递归?
程序调用自身的编程技巧称为递归(recursion);递归作为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于:把大事化小
正确的函数递归的两个必要条件:
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
举例(不考虑程序崩溃带来的影响,下面是一个错误的函数递归)
#include int main(){printf("Hello world!\n");main();return 0;}
举例:接收一个整型值(无符号),按照顺序打印它的每一位。例如:输入:1234,输出 1 2 3 4。
思路:
1234中最容易得到的数字是个位上的数字
1234%10=4
1234/10=123
123%10=3
…
1%10=1
递归就是递推+回归
上面的操作是递推,那怎么回归呢?最后一步得到了1打印,回归到上一步得到的2,打印2…最后回归到第一步得到的4,打印。到此递推+回归完毕。
实现:
#include void Print(int n){if (n > 9)Print(n / 10);printf("%d ", n % 10);}int main(){int num = 1234;Print(num);//Print(1234)//Print(123)+打印4//Print(12)+打印3//Print(1)+打印2//打印1return 0;}
注:红色的路线为递推路线,蓝色的路线为回归路线。每一次函数调用,内存都要开辟空间,这块空间就叫作函数栈帧。
举例:编写函数不允许创建临时变量,求字符串的长度(模拟实现strlen)
注:size_t也是一种无符号整型,size_t是为sizeof这个操作符设计的,打印格式为%zd。
#include size_t My_strlen(char *str)//地址拿指针变量接收{size_t count = 0;//记录字符个数while (*str != '\0'){count++;str++;}return count;}int main(){char arr[4] = "abc";size_t len = My_strlen(arr);//数组名是数组首元素的地址printf("%zd\n", len);return 0;}
注:当str刚开始存的是数组首元素的地址(’a’)时str++等于str的地址为数组第二个元素的地址(’b’)。
改进:因为不满足题意(不创建临时变量),所以采用递归试试
思路:
My_strlen(“abc”)
1+My_strlen(“bc”)
1+My_strlen(“c”)
1+My_strlen(“”)
0
实现:
#include size_t My_strlen(char* str){if (*str != '\0')return 1 + My_strlen(str + 1);elsereturn 0;}int main(){char arr[4] = "abc";printf("%zd\n", My_strlen(arr));return 0;}
思考如果将str+1改成str++会发生啥?
递归与迭代
举例:求n的阶乘
思路1:
递归:
f(n)= 1(n=1或者n=0)
f(n)= N *f(n-1)(n>1)
实现:
#include int Fac(int x){if (x > 1)return x * Fac(x - 1);elsereturn 1;}int main(){int n = 0;scanf("%d", &n);printf("%d\n", Fac(n));return 0;}
思考如果n很大呢?每一次调用都会开辟栈区的空间,是不是就会造成栈溢(stackoverflow)?
迭代:
#include int Fac(int x){int i = 0;int r = 1;for (i = 1; i <= x; i++){r *= i;}return r;}int main(){int n = 0;scanf("%d", &n);printf("%d\n", Fac(n));return 0;}
思考迭代会有bug吗?如果r超出了int类型可以表示的最大值了呢?怎么修改?
举例:求第n个斐波那契数。(不考虑溢出)
思路:
递归
f(n)= 1(n=1或者n=2)
f(n)= f(n-1)+ f(n-2)(n>2)
实现:
#include int Fib(int x){if (x > 2){return Fib(x - 1) + Fib(x - 2);}elsereturn 1;}int main(){int n = 0;scanf("%d", &n);printf("%d\n", Fib(n));return 0;}
若求第50项
☑️调用次数太多,每次调用都占用栈区空间而且会进行多次重复调用,调用了很多次Fib(46)等等。
改进为迭代:
a从1开始,b从1开始,c等于a+b,再把b赋值给a,c赋值给b…
实现:
#include int Fib(int x){int a = 1;int b = 1;int c = 1;while (x >= 3){c = a + b;a = b;b = c;x--;}return c;}int main(){int n = 0;scanf("%d", &n);printf("%d\n", Fib(n));return 0;}
☑️一般如果用递归会造成栈溢出or效率低下的问题时,可以考虑用迭代的方式解决。还可以使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代nonstatic 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
✔️提示:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。 2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。 3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
函数递归的几个经典问题:
1.汉诺塔问题
2.青蛙跳台阶问题