什么是函数
维基百科中对函数的定义为:子程序
在计算机科学中,子程序是一个大型程序的某部分代码,由一个或多个语句块组成。负责完成某项特定任务,相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
函数的分类
库函数
移植性高、需要大量使用的函数,将它们封装成库构成库函数。如之前所用的printf、scanf等。使用库函数,必须包含#include对应的头文件。
学习库函数可以参考网站:https://cplusplus.com/reference/cstdio/
自定义函数
自定义函数和库函数一样,有函数名、返回值类型和函数参数,但这些东西需要我们自己来设计。(函数设计时,最好让其有较强的单一性,这样可移植性强)
函数结构:
ret_type fun_name(paral, * ) { statement;//语句项/函数体 }
ret_type 返回类型 fun_name 函数名 paral 函数参数
但有一点需要注意,自定义函数前面的返回类型必须和函数体内的return类型一致。
为什么要说最好让其有较强的单一性呢?这边举个例子:
要求设计一个函数,让它比较出两个数字谁更大,因此可以有如下函数:
int get_max(int x, int y) { int z = 0; if(x > y) z = x; else z = y; return z; }
此时主函数则只需要一行代码即可输出两者中较大的那一个:printf("MAX = %d",get_max(2,5));也有人会为了让主函数看起来更简洁,直接将printf弄到get_max函数内,此时主函数只需要调用get_max即可。但如果,某个函数只需要比较出两个数谁更大,然后调用那个较大的数字,此时get_max函数里面的printf就显得很多余了,若要使用,又得重新写一个新代码,较为繁琐。所以,保持一个函数的独立性很重要,让它只完成自己该做的那一件事就行了。
函数参数
实际参数(实参):真实传递给函数的参数。实参可以是:常量、变量、表达式、函数等。无论实参是什么样的形式,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。简单来说,实参就是调用函数时输入的参数。
形式参数(形参):指函数名后括号中的变量,形参只有在函数被调用的过程中才实例化(分配内存单元)。简单来说,形参就是调用的函数里的非全局变量。当函数调用完成之后,形参自动销毁。因此形参只在函数中有效。
函数的嵌套调用和链式访问
函数的调用
传值调用:将实参的值传递给形参,函数的形参和实参分别占用不同内存块,对形参的修改不会影响实参。例如:
#include<stdio.h> //函数,找两个整数中的较大值 int get_max(int x, int y); //此处的x,y为形参 int main() { printf("max = %d", get_max(3, 5)); } int get_max(int x, int y) //此处传递的x与y的值,为传值调用 { int z = 0; if(x > y) z = x; else z = y; return z; }
但如果某个函数需要同时修改两个值并将修改后的两个值返回,那传值调用则不再适合。因为一个函数里同时只能返回一个值,所以此处则需要传址调用。
传址调用:把函数外部创建的变量的内存地址传递给函数参数。这种传参方式可以让函数和函数外边的变量建立起真正的联系,即函数内部的形参可以改变函数外部实参所对应的变量。例如:
#include<stdio.h> //交换两个整型变量的值 void swap(int* x, int* y); //传递参数为地址 /* 函数不能同时返回两个值 若选用传值调用,其在函数内部确实有交换, 但函数调用完成后外部的变量并不会跟着改变 */ int main() { int a = 3; int b = 7; printf("a = %d, b = %d\n", a, b); swap(a, b); printf("a = %d, b = %d\n", a, b); } void swap(int* x, int* y) //传址调用 { int z = 0; z = *x; *x = *y; *y = z; }
此处没有返回值是因为其在swap函数内部通过地址对外部参数进行了修改,所以不再需要返回值。
若函数需要改变外部的变量值,则采用传址调用;若只是需要利用外部的值,不对其修改,则采用传值调用。
函数的嵌套调用
函数不能嵌套定义,但可以嵌套调用。即在一个子函数里调用另一个子函数。
链式访问
即把一个函数的返回值作为另一个函数的参数。
函数的声明和定义
由于编译器是从前往后扫描,所以主函数在调用子函数之前,需要在调用之前声明该子函数,或者在主函数之前定义该子函数。若在调用前声明过该子函数,则子函数可以放在主函数之后定义。声明是告知,定义是设计。
声明:告诉编译器有一个函数叫什么、参数是什么、返回值类型是什么。但其是否存在无关紧要。先声明再使用。函数的声明一般放在头文件中。而定义放在对应的.c文件中。
定义:由用户进行设计该函数,需要哪些参数、用这些参数干些什么事情、是否要返回某些值。
函数递归
什么是递归?
程序调用自身的编程技巧称为递归(recursion)。函数不仅可以嵌套定义,还可以嵌套调用,即在调用一个函数的过程中,函数内部又调用另一个函数,而函数的递归调用指的是在调用一个函数的过程中又直接或间接地调用该函数本身。它通常把一个大型复杂的问题层层转化为一个与原问题相似但规模较小的问题来求解,递归策略只需要少量的程序就可以描述出解题过程所需要的多次重复计算,大大的减少了程序的代码量。递归的主要思考方式在于:把大事化小。
#include<stdio.h> int main() { printf("haha\n"); main(); return 0; }
这就是一个递归函数,但它本身是错误的,其运行到一定时候,它会自动停止并报错:栈溢出。
递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续
- 每次递归调用之后越来越接近这个限制条件
即使两者都满足,但也会出现一种错误:栈溢出。
内存分为栈区、堆区和静态区。栈区存放局部变量、函数形参、返回值;堆区是由程序员分配内存和释放;静态区存放全局变量和静态变量。
int test(int n) { if(n < 100000) { text(n + 1); } } //栈溢出 int main() { printf("%d",test(1)); return 0; }
主函数会分配一块栈,称为main的栈帧空间,而运行到下面的test后又会给test分配一块栈帧空间,test条件满足,则又会分配一块栈帧空间给第二个test……因为内存是有限的,栈区总有被分配完的时候,但还有程序需要分配栈帧空间,此时就栈溢出。
所以我们在写递归函数是,需要注意以下两点:
- 不能死递归,要有跳出条件,且每次递归要接近跳出条件,否则会出现栈溢出
- 递归层次不能太深,否则会出现栈溢出