函数
- 前言
- 一、函数是什么
- 二、C语言中函数的分类
- (一)库函数
- 1.printf类
- 2.strcpy类
- 3.math类
- 4.概念
- 5.小知识
- 6.总结
- (二)自定义函数
- 1.概念
- 2.函数的组成
- 3.例子1(求出两个数中的最大值)
- 4.例子2(交换两个整数变量的内容)
- 5.小知识
- 6.总结
- 三、函数的参数
- (一)实际参数(实参)
- (二)形式参数(形参)
- 四、函数的调用
- (一)传值调用
- (二)传址调用
- (三)练习
- 1.写一个函数打印100到200之间的素数
- 2.写一个函数输出1000年到2000年的闰年
- 3.写一个函数,实现一个整形有序数组的二分查找
- (1)详细解题思路:
- (2)小知识:
- (3)代码:
- 4.写一个函数,每调用一次这个函数,就会将num的值增加
- (四)函数调用实质
- 五、函数的嵌套调用和链式访问
- (一)嵌套调用
- (二)链式访问
- 1.概念
- 2.常见例子:
- 六、函数的声明和定义
- (一)函数声明
- 1.概念
- 2.解析
- (二)函数定义
- (三)拓展(模块化开发)
- 七、函数递归
- (一)什么是递归
- 1.概念
- 2.简单代码
- (1)史上最简单递归
- (2)出现的问题
- (3)推荐的网站
- (二)递归的两个必要条件
- 1.概念
- 2.经典练习题1
- (1)思路
- (2)代码
- (3)分析
- 3.经典练习题2
- (1)思路
- (2)代码
- (3)分析
- (三)递归与迭代
- 1.经典练习题3
- (1)思路
- (2)代码
- (3)分析
- 2.迭代概念
- 3.经典练习题4
- (1)思路
- (2)代码
- (3)分析
- (4)问题
- (5)迭代做法
- 4.改进
- 5.解决方法
- 6.提示
- (四)递归的经典题目
- 1.汉诺塔问题
- (1)题目描述和思路
- (2)代码
- 2.青蛙跳台阶问题
- (1)题目描述和思路
- (2)代码
- 总结
前言
函数相信大家并不陌生,那函数具体是什么呢?函数很常见,就是大家平时用的函数,利用几个函数的分装过后,程序就会很清晰,很简单地运行,以下,听我细细讲解一下函数吧!
一、函数是什么
维基百科中对函数是这样定义的,它是一个子程序。
在计算机科学中,子程序,是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定任务,而且相较于其他代码,具备独立性。
一般会有输入参数并有返回值,提供对过程的封装和对细节的隐藏,这些代码通常被称为软件库。
那我们利用 MSDN app看一看函数吧!
二、C语言中函数的分类
(一)库函数
1.printf类
当我们编译完程序后,我们迫不及待地想要知道结果,想把这个结果打印在屏幕上一看的时候,这个时候就非常需要一个函数将这个信息按照一定的格式打印在屏幕上,这就是printf,与之相关联的是scanf(输入)。
2.strcpy类
在编译的过程中我们会频繁地做一些字符串拷贝(strcpy),比较(strcmp)和计算长度(strlen)这类函数。关于如何简单明了的知道这类库函数的使用,我放三个博客在下面大家可以去看看。
模拟strcpy函数的使用
模拟strcmp函数的使用
模拟strlen函数的使用
3.math类
在编译的过程中,我们也会进行计算,电脑计算能力一流,原因是因为它运算速度快,那我们需要引进会计算的库函数来帮助我们进行计算,例如pow函数可以计算一个数的n次方。
4.概念
标准库 - 提供了C语言的库函数。
如果大家都运用这些函数,都用的是printf,strlen,pow这类函数,那是不是直接放到标准库里面,我们用的时候只需要去一调用,直接一行代码就可以使用了,是非常方便的,提高了可移植性和提高程序的效率。那假如说同样都是pow函数,中国程序员写:ncifang(),而英国程序员写:pow(),这样是不是有差异了,中国程序员看不懂英国程序员写的,英国程序员看不懂中国程序员写的,这可移植性是很差的,写的程序给别人别人看不懂那一大串是啥意思,这时候标准库的出现解决了这一问题,只需要有一样的标准即可,大家都可以调用这个函数且大家都能看懂,方便多了。
5.小知识
如何学习库函数的使用:
法一:下载MSDN软件,进行索引查询
法二:访问库函数查询网站
法三:访问库函数查询网址2(英文版)或库函数查询网址3(中文版)
6.总结
1.C语言常用的库函数有:
I/O函数 – input/output 输入输出函数 例如:scanf,printf
字符串操作函数 – strlen,strcmp,strcpy
字符操作函数 – 大小写转换/字符分类
内存操作函数 – memcpy,memmoove,memset
时间/日期函数 – time
数学函数 – pow,sqrt
其他库函数
2.要借助工具学习库函数,在小知识中已经包含了。
3.英文很重要,要看得懂文献。
(二)自定义函数
1.概念
要是所有都可以使用库函数,都从一个标准库里面能够调出来,那程序员不是失业了吗?所以随之而来的是有了自定义函数,就是程序员可以自己写函数并进行调用,但是函数名需要让大家能够看懂,如果写了几千行代码的函数,其函数名叫a,好家伙,程序员得看几千行代码才能知道这个a函数是干啥的,那如果你命名很通俗易懂,那很简单,程序员拿到函数的程序,稍微一检查,直接调用,是不是很方便呢?
2.函数的组成
ps:(*)表示还有很多,是等等的意思。
3.例子1(求出两个数中的最大值)
代码如下:
#include<stdio.h>
//写一个函数可以找出两个整数中的最大值
//定义函数
int Max(int x,int y) {
if (x > y) {
return x;
}
else {
return y;
}
}
int main() {
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b); //推演函数的使用场景
//找出a和b中的最大值
int max = Max(a, b); //传参
//打印
printf("%d\n", max);
return 0;
}
以下是函数框架所代表的信息,与函数的组成相一致:
以下为函数调用过程图:
调试结果如下:
4.例子2(交换两个整数变量的内容)
这个例子不用函数编写其实很简单,但是一旦利用函数那么就要有所讲究了,不能是简单的传值的操作,而应当是传地址的操作。这一部分涉及到了函数的栈帧的创建与销毁,进入调试窗口你会发现底下两个变量的地址与进入函数的那两个变量的地址不相同,大家有兴趣可以去看一下函数栈帧与创建的博客。
函数栈帧的创建与销毁
相信大家看完那篇博客,已经对函数有了更深的理解,接下来,看一个错误示范寻找一下问题。
修改了x,y之后怎样,a和b的值没有进行改变,你只是交换的是形参的值,在内存中形参也是有地址存储的,它们进行改变没有影响实际参数的值,所以结果不变,可以说形参只是实参的一份临时拷贝是完全没有错误的。
那怎么做呢?那就用取地址呀,取到那个地址,使函数外部变量与函数内部变量产生联系,在同一个空间进行交换不就能做了吗?大家可能会有点疑惑,这个指针似乎不理解,其实大家只需要知道取了那个变量的地址存放到另一个变量中,在对这个变量进行修改即可,即:
如果大家还想提前深入了解指针,我放一篇博客供大家先去预习一下。
初阶指针
接下来我们回来,进行交换值的操作:
其实设计的很巧妙,是把a,b的地址找两个变量进行存放,再通过这个地址去找到a,b这两个变量,引入tmp进行交换。相当于给两个人两个门牌号,通过这个门牌号去找到房间里面的人并进行交换。
代码如下:
void Swap(int* x, int* y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
int main() {
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//交换前
printf("交换前:a=%d b=%d\n", a, b);
//交换
Swap(&a, &b);
//输出
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
5.小知识
Q1.如何调出监视窗口?
答:先按F10,再去找如下图所示:
Q2.如何进入函数?
答:按F11进入函数内部。
Q3.为什么我的F10和F11按了以后锁屏幕了!?
答:因为刚开始的电脑没那么多的娱乐化按键,大多数都是进行编译用的,但电脑逐渐普及了以后,电脑生产厂商看大家都不怎么用这几个按键,那好嘛,就全部换成了调节声音,调节屏幕亮度,锁屏等此类的按键了,那怎么解决这个问题呢?其实很简单,按Fn+F10即可,因为Fn为辅助按键,一起按电脑才知道原来执行的是这个命令。
6.总结
在进行函数的使用的时候,一定要根据函数的构成每一步都不能落下,一旦落下,编译器就会报错。那当然,如果你不想传参或者不需要返回值,那很简单,写一个void哦,千万不能不写,返回类型不写的话编译器默认返回值是int类型,而参数那里什么都不写的话,编译器直接报错。
例如:
三、函数的参数
(一)实际参数(实参)
真实传递给函数的参数,叫实参。
实参可以是:变量、常量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
(二)形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的时候才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
形参实例化之后其实相当于实参的一份临时拷贝。
四、函数的调用
(一)传值调用
函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参,就例如上面所讲的Max函数,是传值调用。
(二)传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。就例如上面所讲的交换两个整数变量的内容的Swap函数一样。
(三)练习
1.写一个函数打印100到200之间的素数
//输出100到200之间的素数
#include<stdio.h>
#include<math.h>
is_prime(int i) {
int j = 0;
for (j = 2; j < sqrt(i); j++) {
if (i % j == 0) {
return 0; //不是素数
}
}
return 1; //是素数
}
int main() {
int i = 0;
for (i = 100; i <= 200; i++) {
//判断i是不是素数
if (is_prime(i)) {
printf("%d ", i);
}
}
return 0;
}
2.写一个函数输出1000年到2000年的闰年
//写一个函数判断一年是不是闰年
//
//1.能被4整除,并且不能被100整除为闰年
//2.能被400整除为闰年
//
int is_loop(int i) {
if ((i % 400 == 0) || (i % 100 != 0) && (i % 4 == 0)) {
return 1;
}
else {
return 0;
}
}
int main() {
int i = 0;
for (i = 1000; i <= 2000; i++) {
//判断i是不是闰年
//是闰年返回1
//不是闰年返回0
if (is_loop(i)) {
printf("%d ", i);
}
}
return 0;
}
3.写一个函数,实现一个整形有序数组的二分查找
(1)详细解题思路:
(2)小知识:
sizeof是求元素的占内存空间的字节长度。
(3)代码:
//写一个函数,实现一个整形有序数组的二分查找
#include<stdio.h>
int binary_search(int arr[], int k, int sz) {
int left = 0;
int right = sz - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] < k) {
left = mid + 1;
}
else if (arr[mid] > k) {
right = mid - 1;
}
else {
return mid; //返回下标
}
}
return -1; //找不到
}
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 0;
scanf("%d", &k); //输入需要找的值
int sz = sizeof(arr) / sizeof(arr[0]); //算出数组元素个数,数组总体元素的字节长度除以数组第一个元素字节的长度
//找到就返回下标
//找不到返回-1
int pos = binary_search(arr, k, sz);
if (-1 == pos) {
printf("没找到\n");
}
else {
printf("找到了,下标为:%d\n", pos);
}
return 0;
}
4.写一个函数,每调用一次这个函数,就会将num的值增加
//写一个函数,每调用一次这个函数,就会将num的值增加
#include<stdio.h>
void Add(int* p) {
//*p = *p + 1;
(*p)++;
}
int main() {
int num = 0;
Add(&num);
printf("%d\n", num); //1
Add(&num);
printf("%d\n", num); //2
Add(&num);
printf("%d\n", num); //3
return 0;
}
(四)函数调用实质
就好像,你在看一个电影,看到20分钟的时候,妈妈叫你去打瓶酱油,你先摁下暂停键,再去打酱油,打完酱油回来再摁开始键继续看电影,这是一个顺序的问题,在一个时间轴上,只有干完这件事,才继续去干原先是事。
五、函数的嵌套调用和链式访问
(一)嵌套调用
先举个小栗子:
函数之间是可以互相调用的,调用了一个函数以后,我们又需要在这个函数里再调用一个函数进行另外的计算,但是切记,函数不能嵌套定义,就是不能在函数内部再定义一个函数(在一个函数内部再写一个函数的内容),因为每个函数之间都是平等的,是平等的关系。
(二)链式访问
1.概念
定义:把一个函数的返回值作为另一个函数的参数。
举个例子:
2.常见例子:
int main() {
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
大家可以想想这个奇怪的式子打印出来的是什么?
接下来分析一下,大家有了上面概念的基础的加持,知道打印的是另一个函数的参数,那就是printf的参数咯!那我们打开MSDN一看关于printf函数的描述和返回值。
发现,printf的返回值是所指向的字符数,那就好办了,先打印43,43俩字符,打印2,2一个字符,再打印1。
六、函数的声明和定义
(一)函数声明
1.概念
1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2.函数的声明一般出现在函数的使用之前。要先声明后使用。
3.函数的声明一般要放在头文件中的。
4.声明中可以不加形参的名字,只要形参的类型即可。
2.解析
大家可能在看书或者做题的时候不像我上面写的那些函数那样,很多情况都是以下代码:
#inclue<stdio.h>
//函数的声明
int Add(int x, int y);
int main() {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
int Add(int x, int y) {
return x + y;
}
很简单,Add(int x, int y);这个就是函数的声明,那这个声明有什么用呢,或者怎么用呢?接下来我将细细分析!
(二)函数定义
函数的定义是指函数的具体实现,交代函数的功能实现,而函数定义也是一种特殊的声明。
//函数的定义
int Add(int x, int y) {
return x + y;
}
int main() {
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
(三)拓展(模块化开发)
函数的声明和定义在很多地方不像上面所述的那么简单,而在工程当中是需要创建头文件和其他工程的,如下图所示:
创建三个文件,一个头文件,两个工程文件,即两个模块(add.c+add.h和test.c):
函数的主要部分放到test.c中:
函数的定义放到add.c中:
函数的声明放到头文件add.h中:
模块化开发的优势:
大家如果对这个模块化开发很感兴趣的话,可以看一下我写的三子棋和扫雷游戏的博客:
三子棋
扫雷
七、函数递归
(一)什么是递归
1.概念
程序调用自身的编程技巧称为递归。
递归作为一种算法在程序设计语言中广泛应用,一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法。
特点:它通常是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小。
2.简单代码
(1)史上最简单递归
(2)出现的问题
(3)推荐的网站
stackoverflow.com
《程序员问答社区》(国外)
segment.com
《思否》(国内)
(二)递归的两个必要条件
1.概念
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
2.经典练习题1
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:输入1234 输出1 2 3 4
(1)思路
(2)代码
#incldue<stdio.h>
void Print(unsigned int num) {
if (num > 9) {
Print(num / 10);
}
printf("%d ", num % 10);
}
int main() {
unsigned int num = 0;
scanf("%u", &num);
//写一个函数打印num的每一位
Print(num);
return 0;
}
(3)分析
3.经典练习题2
模拟strlen函数,求字符串长度。
给大家分享一篇博客,是关于详解strlen函数的博客!
模拟strlen函数
(1)思路
不创建临时变量,只要第一个字符不是‘\0’,则拿出第一个字符,并求其他字符,递归下去:
arr+1表示指针指向往后移一位,*arr表示访问指向的这块空间。
(2)代码
#include<stdio.h>
int my_strlen(char* arr) {
if (*arr != '\0') {
return 1 + my_strlen(arr + 1); //指针指向往后移一位
}
else{
return 0;
}
}
int main() {
char arr[] = "bit";
//{b i t \0}
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
(3)分析
(三)递归与迭代
1.经典练习题3
求n的阶乘
(1)思路
n!可以转化为n*(n-1)!来进行计算。
(2)代码
//写一个函数求n的阶乘
#include<stdio.h>
int Fac(int n) {
if (n <= 1) {
return 1;
}
else {
return n * Fac(n - 1);
}
}
int main() {
int n = 0;
scanf("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
(3)分析
大家可能有疑问了,这个n为什么调用完变了,返回的时候那个n还是原先那个值,不是应该改变的吗?这其实涉及到了一个比较难的知识,是函数栈帧的创建与销毁,大家可以先去看看我写的关于函数栈帧创建与销毁的博客:
函数栈帧的创建与销毁
以下是简单介绍,大家看完介绍可以去看看函数栈帧的创建与销毁的那篇博客。
2.迭代概念
如果产生很大的数字,超过42亿的数了,用递归程序就容易崩掉,这是由于出现栈溢出的问题,所以就出现了迭代的思想,而迭代是什么,迭代其实就是循环,栈溢出的现象就会迎刃而解。
3.经典练习题4
求第n个斐波那契数(兔子繁殖问题)(不考虑栈溢出问题)
(1)思路
(2)代码
//求第n个斐波那契数(兔子繁殖问题)
//前两个数相加等于第三个数
#include<stdio.h>
int Fib(int n) {
if (n <= 2) {
return 1;
}
else {
return Fib(n - 1) + Fib(n - 2);
}
}
int main() {
int n = 0;
scanf("%d", &n);
//写一个函数求第n个斐波那契数
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
(3)分析
(4)问题
递归解法太低效了,算n=50,需要好久。
我们可以看一下n=40时第三个斐波那契数需要被计算多少次:
(5)迭代做法
计算效率过于低,但是可以写成循环的方式,也就是迭代,速度非常快。
如下:前两个值相加赋给第三个值,然后第二个值赋给第一个值,第三个值赋给第二个值,一直加到相应的n的值。
#include<stdio.h>
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);
//写一个函数求第n个斐波那契数
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
4.改进
1.在调试factorial 函数的时候,如果你的参数比较大,那就会报错:stackoverflow(栈溢出)这样的信息。
2.系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
5.解决方法
1.将递归改写成非递归。
2.使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。
6.提示
1.许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2.但是这些问题的选代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3.当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
4.递归可以转化为迭代(循环)(非递归),这是递归与非递归之间的转换。
(四)递归的经典题目
1.汉诺塔问题
(1)题目描述和思路
描述:一次移动一个方块,将A柱子上面的方块全部移动到C柱子上,前提是大的方块不能压在小的方块下面。
思路:三个方块:1->C,2->B,1->B,3->C,1->A,2->C,1->C。
四个盘子就是三个盘子的递归加一个盘子,依次类推。
(2)代码
#include<stdio.h>
void move(char A, char C, int n)
{
printf("把第%d个方块从%c--->%c\n", n, A, C);
}
void HanoiTower(char A, char B, char C, int n)
{
if (1 == n)
{
move(A, C, n);
}
else
{
//把n-1个方块从A柱借助于C柱移动到B柱上
HanoiTower(A, C, B, n - 1);
//把A柱子上最后一个方块移动到C柱上
move(A, C, n);
//把n-1个方块从B柱借助于A柱移动到C柱上
HanoiTower(B, A, C, n - 1);
}
}
int main()
{
int n = 0;
printf("输入A柱子上的方块个数>\n");
scanf("%d", &n);
//将n个方块从A柱借助于B柱移动到C柱上
HanoiTower('A', 'B', 'C', n);
return 0;
}
具体我会出一篇博客细细讲解一下汉诺塔问题。
2.青蛙跳台阶问题
(1)题目描述和思路
描述:有n个台阶,青蛙一次可选择跳1个台阶或者2个台阶,那总共有多少种跳法?
思路:
其实就是一个斐波那契数列。
(2)代码
代码如下:
#include<stdio.h>
int Add(int n) {
int a = 1;
int b = 2;
int c = 0;
while (n > 2) {
c = a + b;
a = b;
b = c;
n--;
}
return c;
if (n <= 2) {
return n;
}
}
int main() {
int n = 0;
//输入总共有多少台阶
printf("请输入总共有多少个台阶>");
scanf("%d", &n);
//调用一个函数青蛙跳台阶次数
int ret = Add(n);
//打印总共跳了多少次
printf("青蛙总共跳了多少次:%d\n", ret);;
return 0;
}
int Add(int n) {
if (1 == n) {
return 1;
}
else if (2 == n) {
return 2;
}
else {
return Add(n - 1) + Add(n - 2);
}
}
int main() {
int n = 0;
//输入总共有多少台阶
printf("请输入总共有多少个台阶>");
scanf("%d", &n);
//调用一个函数青蛙跳台阶次数
int ret = Add(n);
//打印总共跳了多少次
printf("青蛙总共跳了多少次:%d\n", ret);;
return 0;
}
具体关于汉诺塔问题和青蛙跳台阶问题我会再出一篇博客细细讲解。
总结
如果说指针是C语言的灵魂,那么函数就是C语言的根本,函数在C语言当中是很重要的,我们重点分析了递归,递归难度很大,要慢慢理解。这一单元,我们详细了解了函数就是在栈区开辟一块属于自己的空间并进行运算,大家最需要了解到的是函数栈帧的创建与销毁,还有就是需要看一部分指针的内容,可以不用看的很熟练,后续的指针单元讲解大家会很轻松的掌握指针的。
客官,码字不易,来个三连支持一下吧!!!