一、什么是函数?
维基百科中对函数的定义:子程序。
在计算机科学中,子程序是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定的任务,具有一定的独立性。
注意:关于函数之间的关系
① main()
函数是主函数,也是一个函数,但无论是什么函数,函数与函数之间都是并列关系,它们之间可以相互调用,但关系仍然是并列的,(函数2对函数1说:我可以去帮你,但你不能妄想拥有我!)
② 此图中,函数1
,2
,3
之间都是并列的关系,函数2
,3
是前来帮助函数1
(main
函数)的,而不是函数1
拥有函数2
,3
。
同理,如果函数2
中也有地方需要用到计算差的功能,它也可以找函数3
帮忙(调用)。
二、函数基本形式
函数基本内容有:返回值类型,函数名称,形参,返回值。
-
函数名称: 一个函数必须要有名字,不然很多函数无法辨别,而且想要调用这个函数总得需要一个名字。(就像人得有名字一样,不然人与人直接怎么相互称呼,相互辨别啊!)
-
形式参数(形参): 传入这个函数的数据。比如,一个计算两个数字和的函数,我想要通过这个函数计算和,就要知道到底要计算谁和谁的和,那就需要你给我两个数,然后我通过这个函数来给你计算,所以,算
7
和5
的和,那我要把7
,5
这两个数据传给计算和的这个函数,形参就是起传递数据的作用的。 -
返回值: 通过函数运算后,想要传回的数据。比如,上个例子,我将
7
,5
传给计算和的函数后,在该函数中经过运算,得到了一个结果——它们的和12
,现在需要将这个和返回给之前调用该函数的地方,那就需要一个返回值了,我们正是要返回和12
。 -
返回值类型: 就是返回值的类型,根据返回值来确定。比如,上个例子,我们返回
12
,就是一个int
类型的数据,所以我们这个函数的类型(同时也是返回值类型)也就是int
型的了。
三、C语言中函数的分类
-
库函数:
别人写的,可以直接用。 -
自定义函数:
自己写的,根据自己的需要自定义写函数。
1.库函数
C
语言中把常用的功能进行了封装,封装成一个个的函数,提供给大家使用。
库函数的规定和实现
C
语言并不直接实现库函数,而是提供C
语言标准和库函数的约定,由编译器具体实现各种库函数。
C
语言规定了一个函数的功能,函数名称,形式参数,返回值类型。
(相当于,用户想要一辆车,C语言提供车的标准:车长,车宽,车的颜色…但并不具体造这辆车,让谁造呢?让编译器来具体造出实体车给用户)
注意: 在不同编译器中,使用一个相同的函数,功能可能一样,但内部功能实现的逻辑可能不同。
库函数的使用
1. 使用必须加头文件 :
因为库函数不是我们自己写的,比如:使用printf()
函数,需要加上 #include<stdio.h>
int main()
{
//使用printf()的库函数
printf("hehe!\n");
return 0;
}
常用库函数分类
IO
函数,字符串操作函数,字符操作函数,内存操作函数,时间/日期函数,数学函数,其他库函数
2.自定义函数
与库函数的区别就是,函数的名称,形参,返回值类型都可以自己定义。(自己的函数,想叫什么名字叫什么名字,想设置几个参数就设置几个参数)
自定义函数分类
- 值传递: 传递给函数数据,不改变实参
- 址传递: 传递给函数地址,可以改变实参
值传递
下面看一段代码,理解一下:
#include<stdio.h>
int mul(int num1, int num2)
{
int m = num1 * num2;
return m;
}
int main()
{
int a = 3;
int b = 6;
int c = mul(a, b);
printf("%d\n",c);
}
结果:
代码具体解释:
形参:num1
,num2
用来接收传进函数的数据。
实参:a
,b
传进函数的数据。
此处是值传递的自定义函数,是将 a
,b
的值传给了另外的变量 num1
和 num2
,在函数内部对num1
和 num2
的数据修改不会对 a
,b
的数据造成改变,因为 a
,b
和 num1
,num2
都是不同的变量,num1
和 num2
与 a
,b
的关系仅仅是数值相同(相当于赋值)罢了。
当实参传递给形参时,形参是实参的一份临时拷贝,对形参的修改(变量num1
,num2
),不会影响实参(变量 a
,b
)。
址传递
下面看一段代码,理解一下:
#include<stdio.h>
void swap(int* p1, int* p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int c = 200;
int d = 100;
swap(&c, &d);
printf("c = %d,d = %d\n", c, d);
return 0;
}
结果:
代码解释:
形参:两个指针变量,int* p1
,int* p2
,用来接收传入的 &c
和 &d
两个地址。
实参:&c
,&d
是两个地址,所以用(形参)指针变量来接收。
此处是址传递的自定义函数,正如其名,是参数传递的是地址,通过地址,能找到实参,进而改变实参数。如本例,就是将 c
,d
的地址(&c
,&d
)传入函数,让 p1
,p2
来接收,通过对指针解引用可以访问,修改原来的实参 c
,d
的值,这就是址函数与值传递函数的区别。
定义函数时应该值传递还是址传递
看是否需要改变实参的值,需要改动就定义址传递函数,不需要就定义值传递函数。
3.实参和形参
形参的创建和销毁
- 形式参数只有在调用函数时,才分配空间(栈区),而不调用函数时,只是形式上存在,并没有实例化。
- 在函数调用结束后,形参立即销毁。
- 形参存储在内存中的栈区。
实参
可以是变量,常量,表达式,函数,只是必须是明确的值。
以函数 max()
为例:找出两个数中最大值的一个函数。
四、函数的调用
上述过程中,其实已经包括了函数的调用。
传值调用
传址调用
五、函数的嵌套
在已有的函数中加一个函数进去,叫函数嵌套。
函数可以嵌套使用,但不能嵌套定义。
1.函数定义和函数调用区分
函数定义:创建某个函数。(盖房子)
函数调用:使用某个函数。(住进房子里)
2.嵌套调用和嵌套定义
函数嵌套定义
函数嵌套定义:在一个函数中定义一个函数。
该代码是在 main()
函数中,定义了一个名为 hell()
的函数。可以看出来 hell()
和 main()
两个函数是包含关系,这是不允许的。
函数嵌套调用
函数嵌套调用:在一个函数中使用一个函数。
该代码是在 main()
函数中,调用了一个名为 hell()
的函数。可以看出来 hell()
和 main()
两个函数是并列关系,这是可以的。
六、链式访问
函数链式访问:一个函数的返回值作为另一个函数的参数。
示例:
printf()
函数的返回值是打印数字和字符的个数。
解释:
示例延展:
在每个 printf()
函数中的 %d
后了一个空格,空格也是一个字符。
解释:
七、函数声明
1.什么时候进行函数声明
函数使用(调用)前,都需要函数声明,但函数定义相当于一种函数声明,所以,当函数定义在函数使用的前面,不用进行函数声明,当函数定义在函数使用的后面,那就需要进行函数声明了(这是合理的,你先使用总得告诉编译器一声这是个什么东西吧,不然它怎么知道这是个函数呢,函数声明就是干这个的)。
2.函数声明的格式
形参部分可以直接写类型,不用标明类型名称。如,int sum( int ,int )
;
示例:
3.函数声明和函数定义的区别
声明就是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
4.函数声明一般放在头文件中
一般将函数声明放到头文件中,将函数定义放到源文件中,在函数调用的文件中包含头文件,就可以用了。
八、函数递归
程序调用自身的编程技巧称为递归( recursion),它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递:递推
归:回归
示例:
示例1:
main()
函数中调用 main()
函数
结果:
无限打印 hehe!
,最后死递归了,会栈溢出。
每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
示例2:
接受一个整型值(无符号),按照顺序打印它的每一位,(输入:1234
,输出:1 2 3 4
)
方法1:
利用数组,将该数字的每一位得到,放到一个数组中,然后打印数组。
#include<stdio.h>
int main()
{
int a = 0;
int i = 0;
int arr[10];
scanf_s("%d", &a);
while (a )
{
arr[i] = a % 10;
i++;
a /= 10;
}
for (i = i - 1; i >= 0; i--)
{
printf("%d ", arr[i]);
}
return 0;
}
结果:
方法2:
利用递归函数,定义一个函数,print(unsigned int x)
会打印出无符号整形 x
的每一位。
假设要打印数字 3426
的每一位,就是打印 342
的每一位再打印一个 6
;同理,打印 342
,就是打印 34
,再打印一个 2
就可以了。
#include<stdio.h>
void print(unsigned int a)
{
if (a / 10 == 0)
printf("%d ", a);
else
{
print(a / 10);
printf("%d ", a % 10);
}
}
int main()
{
unsigned int a = 0;
scanf_s("%d", &a);
print(a);
return 0;
}
结果:
示例3:
求第 n
个斐波那契阿数是多少。
方法1:
用递归的方法,第 n
个斐波那契数是第 n-1
和第 n-2
的和,那求第 n
个斐波那契数,就要先求第 n-1
和 第 n-2
个。
#include<stdio.h>
int feibo(int n)
{
if (n == 1 || n == 2)
return 1;
else
{
return feibo(n - 1) + feibo(n - 2);
}
}
int main()
{
int n = 0;
printf("请输入 n 的值:");
scanf_s("%d", &n);
printf("第%d个斐波那契数是:%d\n", n, feibo(n));
return 0;
}
结果:
但是,当 n
的数值过大时,如 n = 40
,程序运行速度会稍慢。
最终,也会有结果,只是速度稍慢而已,n
的值越大,就会越慢。
原因是,在计算第 n
个斐波那契数的时候,会有大量的重复计算,这些重复的计算导致运算速度较慢。
方法2:
利用循环的方式得到第 n
个斐波那契数。
#include<stdio.h>
int main()
{
int n = 0;
int x = 0;
printf("请输入 n 的值:");
scanf_s("%d", &n);
if (n == 1 || n == 2)
x = 1;
else
{
int x1 = 1;
int x2 = 1;
for (int i = 3; i <= n; i++)
{
x = x1 + x2;
x1 = x2;
x2 = x;
}
}
printf("第%d个斐波那契数是:%d\n", n, x);
return 0;
}
结果:
跟递归的方法比起来,此方法计算第40
个斐波那契数计算量要少的多,所以速度要比递归的方法快。
通过上面的示例,我们可以得到下面几个结论:
1. 递归的条件
- 存在限制条件。当满足这个限制条件的时候,递归便不再继续。没有限制条件就会造成死递归。
- 每次递归调用之后都越来越接近这个限制条件。
即上面示例中的最后一层,如打印函数 print()
中,当形参是一位数时,就会直接打印,不再递归;求第 n
个斐波那契数中,当 n == 1
或 n == 2
时,会直接返回 1
,而不是继续递归。
2. 递归与循环的关系
递归可以实现某件事的循环。
循环每次都不一定会开辟新的空间,但递归会,所以递归可能会造成程序的崩溃。
3. 迭代与循环的关系
迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
循环是迭代的一种。
4. 采用迭代还是递归
两种方式各有各的特点,对于某个问题,某种方法可能有明显缺陷,那就用另一种。
如,求第 n
个斐波那契数的问题,采用递归的方法,有大量的重复计算,运算效率较低,所以该问题采用迭代的方法就较好。
本文到这里就结束了,如果对您有帮助,希望得到一个赞!🌷
如果错漏,欢迎指正!😄