函数,简言之,就是实现某种特定功能的一段代码。函数分为库函数和自定义函数。函数具有以下特点:1.封装性。 2.复用性。
一、 库函数
库函数是一些已经配置好功能的一些函数,根据自己的现实需要来选择库函数。因为库函数是在标准头文件中声明的,所以在使用库函数时应该加上对应的头文件。否则程序运行时会不认得这函数。
库函数的学习工具:https://legacy.cplusplus.com/
二、 自定义函数
库函数的功能不能满足功能的实现,所以就需要自己通过写代码来实现这个功能,将它包装成函数,这样的函数就叫做自定义函数。
2.1 结构
ret_type fun_name(形式参数)//函数头 { …………;//(函数体) }- 函数头,包括函数名,参数,返回类型。
- 函数名:尽量有意义。
- 参数:这里的参数叫做形式参数。在调用函数时,传过去的参数叫做实参。
- 返回类型:根据函数返回的数据类型来确定函数返回类型。函数返回类型不写的话,会默认为int 类型。
- 函数体:就是自己实现功能的代码部分。
函数体里面有一个return语句:用来退出函数并返回数据。
- return 语句后面可以是表达式,也可以是函数值。
- 如果函数的返回类型为void类型(不返回任何参数),则无需return 语句,也可以直接写为 "return ;"
- 在if-else语句中应保证每种情况都有返回。
红框框圈住的是我们自定义函数,那里的参数为形参。蓝框框圈住的是函数调用部分(即我们要使用这个函数),括号里的是实参(就是我们传给数据),然后形参收到数据之后用这些数据进行计算。
要保证函数的形参和实参数值和个数要相对应。
2.2 实参和形参的关系
这是两形参的地址。
这是两实参的地址。
可以发现实参和形参虽然数值相同,但是地址并不同。
结论:
- 形参是实参的一份临时拷贝。(为啥叫临时拷贝呢?因为当函数调用结束后就会销毁形参)
- 形参的改变并不会影响是实参的改变。
2.3、 数组作为函数参数
- 数组作为参数,定义数组是什么类型,那么参数就是什么类型。
- 参中可以不写数组大小,只写框框。
- 实参中传数组应该只写数组名(数组传参实际上是地址传参),因此形参不会创建新数组,形参和实参使用的是同一个数组。
- 二维数组类似,只不过二维数组行可以省略,但是列不可以省略。
练习:数组内容全部置为-1,再打印数组。
#include <stdio.h> void arr_value(int arr[10], int len) { int i = 0; for (i = 0; i < len; i++) { arr[i] = -1; } } void print_arr(int arr[], int len) { for (int i = 0; i < len; i++) { printf("%d ", arr[i]); } } int main() { int arr1[10] = {1}; arr_value(arr1, 10); print_arr(arr1, 10); return 0; }三、 函数的嵌套调用
函数嵌套就是函数里又再次调用了另一个函数。
练习:计算某年某月有多少天?
#include <stdio.h> #include <stdbool.h> bool is_leap_year(int year) { if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { return true; } else { return false; } } int get_days(int year,int month) { int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month == 2) { if (is_leap_year(year)) { return 29; } else { return 28; } } else { return days[month]; } } int main() { int month = 0; int year = 0; scanf("%d %d",&year, &month); int days=get_days(year,month); printf("%d\n", days); return 0; }四、 函数的链式访问
链式访问就是一个函数的返回值作为另一个函数的参数。
#include <stdio.h> #include <string.h> int main() { char arr[] = "abcdef"; printf("%zu\n", strlen(arr)); return 0; }五、 函数的声明和定义
5.1 单个文件
- 函数调用:使用函数的过程(相当于用函数的窗口)。
- 函数定义:就是自己定义函数头(函数名,参数,返回类型)以及函数体。
- 函数声明:当函数调用时,程序会从函数调用的地方向上寻找这个函数,如果我们将函数写到下方,那么就会找不到,所以就需要函数声明,告诉程序我有这个函数。函数声明中的参数可以只写数据类型,省略变量。
5.2 多个文件
当我们在做一个项目时,我们往往会使用多个文件,一个是用来函数声明的头文件(.h),一个是用来函数定义的文件(.c),另一个是用来函数调用的测试文件(.c)。
但是在函数调用前要加上头文件如:“Add.h”,这是包含自己创建的头文件。而< >是库函数的头文件,如:<stdio.h>。
5.3 static 与 extern
5.3.1 作用域与生命周期
作用域:创建的变量可用的代码范围;生命周期:变量从创建到被销毁的时间段。
- 局部变量:作用域:仅能在变量所被创建的代码块(用{ }括住的代码)中有效,一旦超出,程序将显示未定义。生命周期:进入作用域局部变量被创建,生命周期开始,出去作用域生命周期结束。
- 全局变量:作用域:整个工程,但是要记住用extern声明一下(不在同一个文件)。生命周期:程序开始,生命周期开始;程序结束,生命周期结束。
5.3 extern
extern是用来声明外部符号的。
5.3.3 static
1.修饰局部变量
//代码1 #include <stdio.h> void test() { int i = 0; i++; printf("%d ", i);//1 1 1 1 1 } int main() { int i = 0; for(i=0; i<5; i++) { test(); } return 0; } //代码2 #include <stdio.h> void test() { //static修饰局部变量 static int i = 0; i++; printf("%d ", i); //1 2 3 4 5 } int main() { int i = 0; for(i=0; i<5; i++) { test(); } return 0; }内存中用栈区,堆区以及静态区。
- 栈区:用来存放局部变量和函数参数。
- 堆区:用来动态内存申请。
- 静态区:用来存放全局变量和静态变量。
当用static来修饰局部变量,局部变量称为静态变量,进入静态区,调用函数,函数销毁但是静态变量并不销毁,仍保持不变,并且在下一次调用时继续接着上一次的值进行计算。
结论:static修饰局部变量,作用域不变,延长生命周期,存储类型改变:栈区->静态区。
2.修饰全局变量
结论:全局变量原本具有外部链接属性,即可以用extern声明就可以在同工程的其他文件进行使用。但是,全局变量一旦被static修饰,外部链接属性就会变为内部,即使有声明也不能使用。只能在本源文件中使用。
3.修饰函数
结论:与全局变量类似,函数也具有外部链接属性。函数一旦被static修饰就仅能在本源文件中使用。
六、 函数递归
函数递归是指函数自己调用自己。递归,递推和回归。
递归的思想就是大事化小的思想,将大问题逐渐拆解成一个个小问题。
6.1 递归的限制条件
- 递归存在限制条件,当满足限制条件时,递推停止,开始回归。
- 每一次递归都在不断地接近限制条件。
6.2 递归举例
练习1:求正整数n的阶乘。
思路:n!=n*(n-1)!(n>=1); n!=1(n=0时)
Fact(n)=n*Fact(n-1);0!=1。
#include <stdio.h> int Fact(int n) { if (n == 0) { return 1; } else { return n * Fact(n - 1); } } int main() { int input = 0; scanf("%d", &input); printf("%d\n",Fact(input)); return 0; }练习2:顺序打印一个整数的每一位。
比如:输入:1234;输出:1 2 3 4
输入:520; 输出:5 2 0
分析:我们如果是逆序打印的话,只需要%10,取到最后一位,/10删去最后一位。运用大事化小的思想来解决,我们最容易取出最后一位,可是,要求先打印第一位,那么就不断/10,直到成为个位数,就可以%10,打印。
#include <stdio.h> void print(int n) { if (n / 10 != 0) { print(n/10); } n = n % 10; printf("%d ", n); } int main() { int input = 0; scanf("%d", &input); print(input); return 0; }6.3 递归与迭代
练习1中既可以用递归写,也可以用迭代写。
//迭代 unsigned int Fact(int n) { int sum = 1; for (int i = 1; i <=n; i++) { sum = sum * i; } return sum; } int main() { int input = 0; scanf("%d", &input); printf("%u\n" ,Fact(input)); return 0; }上述例子中由于递归可能会出现栈溢出,而迭代没有栈溢出的风险,写起来逻辑并不复杂,效率更高,所以练习1优先选迭代。
练习3:斐波那契数列
分析:n=1,2时,Fib(n)=1;n>2时,Fib(n)=Fib(n-1)+Fib(n-2)。、
//递归 #include <stdio.h> unsigned int Fib(int n) { if (n <= 2) { return 1; } return Fib(n - 1) + Fib(n - 2); } int main() { int input = 0; scanf("%d", &input); printf("%u\n", Fib(input)); return 0; } //迭代 #include <stdio.h> unsigned int Fib(int n) { int a = 1; int b = 1; int c = 1; while (n > 2) { a = b; b = c; c = a + b; n--; } return c; } int main() { int input = 0; scanf("%d", &input); printf("%u\n", Fib(input)); return 0; }递归中有好多数都被重复算多次,造成冗余,运行效率较低,所以这个练习迭代较为好。