1.函数是什么
我们在学习数学的时候,经常会用到函数的概念。但是你了解C语言中的函数吗?
- 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
- 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。ps:维基百科对于函数的定义
2.库函数
为什么会有库函数?
- 我们知道在刚开始学习C语言的时候、总是在一段代码编写完成时、想迫不及待的知道它的结果的时候;于是就会使用printf();函数按照一定的格式来打印输出的结果到控制台窗口。
- 在编程的过程中、我们需要频繁的完成一些输入工作(scanf)。
- 在编程的过程中、我们总是要完成n^k的计算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员 进行软件开发。
那么如何学习库函数呢?
可以参照:https://cplusplus.com/reference/
简单的总结,C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
我们参照官方文档来学习几个库函数
char * strcat ( char * destination, const char * source );
int strcmp ( const char * str1, const char * str2 );
需要注意的地方:
在使用某一个库函数时、必须包含#include对应的头文件。
这里对照文档来学习库函数、目的是学会它的用法。
如何学习库函数?3
需要全部记住吗?No
需要学会查询工具的使用:
- MSDN(Microsoft Developer Network)
- www.cplusplus.com http://en.cppreference.com(英文版)
- http://zh.cppreference.com(中文版)
英文很重要。最起码得看懂文献。
3.自定义函数
如果库函数能完成所有的任务,那还要程序员干什么?
所以更加重要的是自定义函数。
自定义函数和库函数一样、有函数名、返回值类型、函数参数。
但不同的是,函数的功能都由我们自己来进行总体的设计、这给了程序员一个很大的发挥空间、
函数的组成部分:
int func(int x,int y) { return x+y; } int //返回值类型 func//函数名 int x,int y//函数参数
我们举一个例子
求两个数的最小公倍数
#include <stdio.h> int gcd(int a, int b) { if(!b) { return a; // (1) } return gcd(b, a % b); // (2) } int main() { int a, b; while(scanf("%d %d", &a, &b) != EOF) { printf("%d\n", gcd(a, b)); } return 0; }
再举一个栗子、计算函数的阶乘和
//1+2!+3!+...+N!的和 (结果为整数形式) #include<stdio.h> long long func(int x); int main() { int a = 0; long long sum = 0; scanf("%d",&a); for(int b = 1;b<=a;b++) { sum+=func(b); } printf("%lld",sum); return 0; } long long func(int x) { if(x==0) return 1; else return func(x-1)*x; }
4.函数参数
4.1 实际参数
真实传给函数的参数叫做实参、参数类型可以是:常量、变量、表达式、函数等。
无论实参是个怎样的值,在进行函数调用的时候、它们必须具有确定的值、以便把这些值传给形参。
4.2形式参数
形式参数是指函数括号内的变量、因为函数只有在调用的时候才会被实例化(分配内存空间)。所以叫做形式参数、而且形参在函数调用完了之后才会销毁,因此形式参数只有在函数中才有效。
上面的gcd和func函数中的参数、a、b、x、都是形式参数、在main函数中传递的参数都是给gcd函数和func函数的实参。给gcd函数的&a、&b和func函数的&a、都是实际参数。
我们对函数的实参和形参做一下分析:
这里可以看到 func 函数在调用的时候, x 拥有自己的空间,同时拥有了和实参一模一样的内容。 所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
5.函数调用
5.2传值调用
- 函数的形参和实参分别占据着不同的内存空间,对形参的修改不会影响实参的值。
5.2 传址调用
- 传址调用是把函数外部创建的变量的内存地址传递给函数参数的一种调用函数的方式
- 这种传参方式使函数内部和外部建立起真正的联系,也就是函数内部就可以直接操作外部的变量
6.函数的嵌套调用和链式访问
函数与函数之间是可以按实际的需求进行合作的、也就是说可以嵌套调用的
6.1嵌套调用
#include <stdio.h> #pragma warning(disable : 4996 int gcd(int x, int y) { if (!y) { return x; } else return gcd(y,x % y); } int my_we(int x, int y) { int sum = 0; sum = (x * y) / gcd(x, y); return sum; } int main() { int x, y; scanf("%d %d", &x, &y); printf("%d %d", gcd(x, y), my_we(x, y)); return 0; } /* 你的代码将被嵌在这里 */
6.2 链式访问
把一个函数的返回值做另一个函数的参数
#include <stdio.h> int main() { printf("%d", printf("%d", printf("%d", 43))); //结果是啥? //注:printf函数的返回值是打印在屏幕上字符的个数 return 0; }
7.函数的声明和定义
7.1函数声明
- 告诉编译器函数名字叫什么、要传入几个参数、返回值类型。但是存不存在这个问题、编译器是决定不了的
- 函数的声明应该放在函数使用之前,需要满足先声明再使用的条件
- 函数的声明一般要放在头文件中的。
7.2 函数定义
- 函数的具体实现,交代函数的功能实现。
add.h中的声明:
1. #pragma once 2. int add(int x, int y); 3. int func(int a);
add.c中的实现 :
int add(int x, int y) { return x + y; } int func(int a)//递归函数举例 { //递推公式 if (a == 1) return 1; return func(a - 1) + a; }
main函数调用:
#include<stdio.h> #include"add.h" int main() { int a; int b; int i; int sum = 0; scanf_s("%d%d%d", &i,&a,&b); printf("%d\n", func(i)); printf("%d\n", add(a, b)); //在这里调用的哦 }
多文件编程的好处是在实现某一个功能的时候、可以把它封装成一个模块。等再次要使用的使用的时候直接调用这个接口即可,在调试的时候也方便了不少,节省了大量的时间重构一个项目,这便叫做高内聚、低耦合。
8.函数递归
- 函数自己调用自身的方法叫做递归( recursion)。
- 递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略
- 只需要少量的代码就可完成大量的重复计算,大大的减小了程序的代码量。
递归的核心思想就是:大事化小
递归的两个必要条件:
- 存在限制条件、当满足这个限制条件时便不会再继续下去
- 每次递归调用之后越来越接近于这个限制条件。
案例1 使用递归求1-100的和 例如 :输入 100 输出5050
参考代码:
#include<stdio.h> int func(int x); int main() { int a; int sum = 0; scanf("%d",&a); sum = func(a); printf("%d",sum); return 0; } int func(int x) { if(x==1) return 1; else return func(x-1)+x; }
案例 2 编写函数不允许创建临时变量,求字符串的长度。
#incude <stdio.h> int Strlen(const char*str) { if(*str == '\0') return 0; else return 1+Strlen(str+1); } int main() { char *p = "abcdef"; int len = Strlen(p); printf("%d\n", len); return 0; }
8.1 递归与迭代
求n的阶乘(不考虑溢出的情况)
int func(int x) { if(x==1) return 1; else return x*func(x-1); }
求第m个斐波那契数列(不考虑溢出)
int func(int x) { if(x<=2) return 1; else return func(x-1)+func(x-2); }
递归算法虽然简洁,但是发现了一个问题;
- 在使用func()这个函数的时候,计算特别大的数时;特别耗时间。
- 使用func()这个函数求10000这个数的阶乘(不考虑溢出),程序会挂掉。
原因在哪里?
func函数在调用时做了很多的重复计算
如果把代码修改一下?
if(n == 3) count++; if (n <= 2) return 1; else return fib(n - 1) + fib(n - 2);
最后输出count,是一个很大的数字
那我们如何改进呢?
- 在调试func函数时,如果你的数字比较大,就会报错:: Stack overflow (堆栈溢出)这样的信息
- 因为系统给栈分配的内存是有限的,如果出现了死循环或(死递归),就有可能导致一直开辟空间,最终产生了栈空间耗尽的情况,这样的情况我们称为栈溢出。
那么如何解决上面的问题?
- 最好的方法就是不使用递归函数
- 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。
留个坑在这(你们自己写哈)
8.2 尾递归
简单来讲,尾递归是指在一个方法内部,递归调用后直接return,没有任何多余的指令了。
比如,一个递归实现的累加函数。
int static func(int x) { if(x==1) return 1; else return func(x-1)+func(x-2); }
请问这个是尾递归吗?答案是错误的!
可能有的人会说,最后一个步骤就是调用func,为什么不是尾递归?
实际上,你看到的最后一个步骤不代表从指令层面来讲的最后一步。这个方法的return先拿到acc(n-1)的值,然后再将n与其相加,所以求func(n-1)并不是最后一步,因为最后还有一个add操作。
如果我们把上面的代码做一个等价替换?
int static func(int x) { if(x==1) return 1; int k = func(x-1); return x+k; }
看,是不是还隐含一个add操作?
累加的尾递归写法是下面这样子的:
int static func(int x,int sum) { if(x==1) { return x+sum; } return func(x-1,sum+x); }
递归调用后就直接返回了,这是真正的尾递归。
递归调用的缺点是方法嵌套比较多,每个内部的递归都需要对应的生成一个独立的栈帧,然后将栈帧都入栈后再调用返回,这样相当于浪费了很多的栈空间.
9.函数作用域规则
9.1 局部变量
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。例如:
int main() { int a,b;//a,b仅在main函数内有效 scanf("%d%d",&a,&b); } int func(int x,int y) { int m,n;//m,n仅在func函数内有效 return x+y; }
说明:
- 1.在main函数中定义的局部变量只能在main函数内进行使用,同时main函数并不能使用其他函数内定义的变量,同时main也是一个函数,与其他函数的地位平等。
- 2.形参变量 函数体内定义的变量都是局部变量,形参传递实参的过程等于给局部变量的赋值操作。
- 3.可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。
- 在语句块中也可定义变量,它的作用域只限于当前语句块,用完即销毁
9.2 全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。例如:
#include<stdio.h> int func(int x,int y); double m,k;//全局变量 int main() { int d,c; scanf("%d%d",&d,&c); } int a,b;//全局变量 int func(int x,int y) { int m,n; return x+y; }
a,b,m,k.都是在函数外部定义的全局变量。C语言代码都是依次从前往后执行的,由于函数func定义在a,b前面所以在func函数内无效,而m,k、都定义在开头、所以在main(),func()两个函数内都是有效的。
全局变量的综合举例:
#include <stdio.h> int n = 10; //全局变量 void func1(){ int n = 20; //局部变量 printf("func1 n: %d\n", n); } void func2(int n){ printf("func2 n: %d\n", n); } void func3(){ printf("func3 n: %d\n", n); } int main(){ int n = 30; //局部变量 func1(); func2(n); func3(); //代码块由{}包围 { int n = 40; //局部变量 printf("block n: %d\n", n); } printf("main n: %d\n", n); return 0; }
运行结果:
func1 n: 20
func2 n: 30
func3 n: 10
block n: 40
main n: 30
代码中虽然定义了多个同名变量 n,但它们的作用域不同,在内存中的位置(地址)也不同,所以是相互独立的变量,互不影响,不会产生重复定义(Redefinition)错误。
- 1) 对于 func1(),输出结果为 20,显然使用的是函数内部的 n,而不是外部的 n;func2() 也是相同的情况。
- 当全局变量和局部变量同名时,在局部范围内全局变量被“屏蔽”,不再起作用。或者说,变量的使用遵循就近原则,如果在当前作用域中存在同名变量,就不会向更大的作用域中去寻找变量。
- func3() 输出 10,使用的是全局变量,因为在 func3() 函数中不存在局部变量 n,所以编译器只能到函数外部,也就是全局作用域中去寻找变量 n。由{ }包围的代码块也拥有独立的作用域,printf() 使用它自己内部的变量 n,输出 40。
- C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。对于 main() 函数,即使代码块中的 n 离输出语句更近,但它仍然会使用 main() 函数开头定义的 n,所以输出结果是 30。
案例二
输入一行字符,统计出其中数字字符的个数。
#include<stdio.h> #include<ctype.h> #include<string.h> //定义全局变量 int number = 0; //数字 //定义统计函数 void len_txt(char s[]) { int c; int i; c = strlen(s); //获取字符串的长度 //分别判断 for (i = 0;i < c;i++){ if (isdigit(s[i])) { number++; } } } //主函数 int main() { char s[100]; //定义字符串最大长度 gets(s); //获取字符串 len_txt(s); //调用函数 printf("%d ",number); return 0; }
输入 Peking University is set up at 1898 输出 4
根据题意,我们希望借助一个函数返回数字字符的个数,为了方便输出结果。我们定义了一个全局变量number来计数,因为函数是void类型没有返回值。全局变量的作用域是整个程序,在函数len_txt中修改变量的值,,能够影响到包括 main() 在内的其它函数。
都看到这了,还不赶紧收藏起来!