前言:函数是构成C语言程序的基本单位。今天就来了解一下C语言中的函数。
什么是函数呢?在数学中有一次函数:y=kx+b。k,b都是常数,只要给一个任意的x就能得到y。在C语言中也引入了函数的概念,也叫做子程序。C语言中的函数就是一个完成某项特定任务的一小段代码。
1 函数的分类
C语言中的函数一般有两类:库函数,自定义函数
。
.
库函数
C语言中是不提供库函数的,C语言的国际标准ANSI C规定了一些常用的函数标准,被称为标准库。标准库里面的函数就叫做库函数。库函数的使用需要包含对应的头文件。
举例说明:求出100~200之间的素数。
#include<stdio.h>
#include<math.h>
int main()
{
int i = 0;
//产生100~200之间的奇数
for (i = 101; i < 200; i += 2)
{
int j = 0;
int flag = 1;//用来判断i是不是素数
//试除法,用2~sqrt(i)去试除i
for (j = 2; j <= sqrt(i); j++)//sqrt是一个库函数,用来求一个数的平方根,需要包含math.h头文件
{
//如果i%j==0,说明i不是素数
if (i % j == 0)
{
flag = 0;//如果i不是素数,flag就置为0
break;
}
}
if (flag)
{
printf("%d ", i);//printf也是一个库函数,需要包含stdio.h
}
}
return 0;
}
输出结果
代码分析:素数是指除了1和它本身之外不能够被其它数整除的数就叫做素数。首先素数肯定不是偶数,因此从奇数里面去找。其次一个数总是能够被拆成两个整数相乘,且其中至少有一个整数是小于等于该数本身的
。例如16=1*16=2*8=4*4
。
.
自定义函数
形式
ret_type fun_name(形式参数)
{
}
ret_type是函数返回类型
fun_name是函数名
举例说明:实现一个加法函数
#include<stdio.h>
int Add(int x, int y)//int是函数返回类型,Add是函数名,x,y是函数参数
{
return (x + y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
输出结果
12 20
32
2 形参和实参
同样的一段代码,我们再来分析一下。
#include<stdio.h>
int Add(int x, int y)//函数的定义
{
return (x + y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int sum = Add(a, b);//函数的调用
printf("%d\n", sum);
return 0;
}
在函数使用的过程中,函数参数分为:实参和形参。
.
在调用Add函数过程中,传递给函数的参数a和b被叫做实际参数,简称实参。
.
在定义Add函数过程中,x和y就是形式参数,简称形参。
为什么叫做形式参数呢?如果只是定义了Add函数,而不去调用Add函数的话,Add函数的参数x,y只是形式上存在,并不会去向内存申请空间,所以叫形式参数。只有在调用Add函数时,为了存放实参传递过来的值,才会向内存申请空间。
.
形参和实参的关系
我们提到了实参是传递给形参的,它们之前是有联系的。但是形参和实参各自是独立的内存空间
。
举例说明:使用函数交换两个整数
#include<stdio.h>
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;
}
输出结果
2 5
交换前:a=2,b=5
交换后:a=2,b=5
很明显,这里a,b的值并没有发生交换。这是为什么呢?这就要说到实参和形参之间的关系了。形参是实参的一份临时拷贝
。可以通过编译器的调试来直接观察:很明显,a,b和x,y的地址是不一样的
。
3 return 语句
注意事项:
.
return后边可以是数值也可以是表达式,如果是表达式则先执行表达式,再返回表达式的结果
。
.
return后边也可以什么都没有,直接写return,这种写法适用于void类型的情况
。
.
return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型
。
.
return语句执行后函数就彻底返回,后边有代码也不会执行
。
.
如果函数中存在if等分支语句,则要保证每种情况下都有return返回,否则会出现编译错误
。
4 数组做函数参数
举例说明:实现一个函数将一个整型数组内容全部置为-1,再实现一个函数将数组内容进行打印。
#include<stdio.h>
void Set_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
arr[i] = -1;
}
}
void Print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);//用来求数组的大小
//设置数组和打印数组都需要知道数组的大小,因此函数的参数中将sz传进去
Set_arr(arr, sz);//设置数组
Print_arr(arr, sz);//打印数组
return 0;
}
输出结果
注意事项:
.
函数的形参与实参个数要匹配
。
.
数组传参,形参是不会创建新的数组的
。
.
形参操作的数组和实参的数组是同一个数组
。
.
形参如果是一维数组,数组大小可以省略不写
。
.
形参如果是二维数组,行可以省略,列不能省略
。
.
函数的实参是数组,形参也可以写成数组形式
。
5 嵌套调用和链式访问
.
嵌套调用
举例说明:计算某年某月有多少天
#include<stdio.h>
#include<stdbool.h>
_Bool is_leap_year(int year)
{
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
int get_year_month(int year, int month)
{
int day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int days = day[month];
if (is_leap_year(year) == 1 && month == 2)
{
days += 1;
}
return days;
}
int main()
{
int year = 0;
int month = 0;
scanf("%d %d", &year, &month);
int days = get_year_month(year, month);
printf("%d\n", days);
return 0;
}
输出结果
2024 2
29
main函数中调用了scanf,printf,get_year_month
,
get_year_month函数中调用了is_leap_year
。
注意事项:函数是允许嵌套调用的,但不允许嵌套定义
。
.
链式访问
链式访问就是将一个函数的返回值作为另一个函数的参数
,像链条一样将函数串起来就是函数的链式访问。
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
输出结果
4321
这段代码的关键就是要清楚printf的返回值。
.
printf返回的是打印在屏幕上的字符个数
。
6 函数的定义和声明
举例说明:输入一个整数,计算组成这个数的数字之和。
例如:
输入:1729
输出:19
19=1+7+2+9
#include<stdio.h>
int DigitSum(int n)//函数的定义
{
int sum = 0;
while (n)
{
sum += n % 10;
n /= 10;
}
return sum;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = DigitSum(n);//函数的调用
printf("%d\n", ret);
}
输出结果
1729
19
上述代码缺少了函数的声明,但是编译器并没有报出警告。这是因为函数定义在函数调用之前。
现在,我们将函数定义放在函数调用之后,看看会不会有什么不同。
#include<stdio.h>
//int DigitSum(int);//函数的声明放在函数调用之前,就可以消除警告
int main()
{
int n = 0;
scanf("%d", &n);
int ret = DigitSum(n);
printf("%d\n", ret);
}
int DigitSum(int n)
{
int sum = 0;
while (n)
{
sum += n % 10;
n /= 10;
}
return sum;
}
很明显,这里编译器报了警告。要想消除警告,必须在函数调用之前,对函数进行声明
。
注意:
.
函数的调用一定要满足先声明后使用
。
.
函数的定义也是一种特殊的声明
。
7 static和extern
在讲解static和extern之前,先来说一下作用域和生命周期。
作用域:通常来说,一段程序代码中所用到的名字并不总是有效的,而限制这个名字的可用性的代码范围就是这个名字的作用域。
.
局部变量的作用域就是变量所在的局部范围。
.
全局变量的作用域是整个工程。
生命周期指的是变量创建到变量销毁之间的一个时间段。
.
局部变量的生命周期:进入作用域变量创建,生命周期开始,出作用域变量销毁,生命周期结束。
.
全局变量的生命周期:整个程序的生命周期。
static修饰局部变量
我们通过对比的方式来观察static有什么作用。
#include<stdio.h>
void test()
{
int i = 0;
//static int i = 0;//static修饰局部变量
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
test();
}
return 0;
}
无static输出结果
1 1 1 1 1
有static输出结果
1 2 3 4 5
代码分析
无static的时候,每次调用test函数,i都会被重置为0,再进行i++的操作,因此每次i的输出结果为1。加了static之后,第一次调用test函数,i为0,再进行i++的操作,此时i为1,第二次调用test函数,i的输出结果为2。这是为什么呢?如果i依然被重置为0,那么输出结果应当为1才是。说明i并没有被重置,而是使用上一次i遗留下来的值
。
结论:static修饰局部变量改变了变量的生命周期,本质上是改变了变量的存储类型。局部变量本来是存储在内存中的栈区,但是被static修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样了,作用域是不会发生变化的。
static修饰全局变量
extern是用来声明外部符号的
。在add.c文件中定义的全局变量要想在test.c中使用,就要使用extern进行声明,否则编译器会报警告。
接下来看static修饰全局变量
可以看到即使extern对全局变量进行了声明,但是编译器依然会报警告。这是为什么呢?
结论:全局变量默认是具有外部链接属性的,被static修饰后,外部链接属性就变成了内部链接属性
,只能在自己所在的源文件内使用,其他源文件即使声明了也无法使用。static修饰函数与static修饰全局变量是一样的。
8 函数递归
什么是递归:递归就是函数自己调用自己。
递归的思想:把一个大型复杂问题转换为一个与原问题相似,但规模较小的子问题来求解。递归的思考方式就是大事化小的过程。递归中的递就是递推的意思,归就是回归的意思。
递归的限制条件
.
递归存在限制条件,当满足这个限制条件的时候,递归便不再继续
。
.
每次递归调用之后,会越来越接近这个限制条件
。
举例说明:求n的阶乘。
#include<stdio.h>
int Fact(int n)
{
if (n == 0)
{
return 1;
}
else
{
return n * Fact(n - 1);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fact(n);
printf("%d!=%d\n", n, ret);
return 0;
}
代码分析
画图分析
9 递归与迭代优缺点
举例说明:求第n个斐波那契数
.
递归实现
#include<stdio.h>
int count = 0;//定义一个全局变量,判断n=3时,需要计算的次数
int Fib(int n)
{
if(n==3)
{
count++;
}
if (n > 2)
{
return Fib(n - 1) + Fib(n - 2);
}
else
{
return 1;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
printf("count=%d\n",count);
return 0;
}
测试分析
可以看到,n=3需要计算的次数是多么庞大。说明使用递归实现求斐波那契数需要很多次重复的计算。这也导致递归的效率低下,时间长
。所以递归并不是万能的,要适可为止。
在C语言中每一次函数调用,都需要为本次函数调用在栈区申请一块内存空间来保存函数调用期间各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。所以递归层次太深,就会浪费太多的栈帧空间,可能会引起栈溢出的问题
。
.
迭代实现
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int a = 1;
int b = 1;
int c = 0;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
printf("%d\n", c);
return 0;
}
代码分析
总结:
递归
优点:
.
代码简洁,结构清晰
缺点:
.
效率低,时间长
.
浪费太多的栈帧空间
应用场景:树形结构,动态规划
等方面。(小编还未学习)
迭代
优点:
.
效率高,时间短
.
无额外内存开销
.
易于理解和实现,逻辑清晰
缺点:
.
代码不够简洁
应用场景:数组遍历,数值计算
等场景。