一、函数的概念
C语言其实是无数个小的函数组成的,可以说一个大的计算机任务可以分成若干个小的函数完成。C语言中有两种函数类型:库函数和自定义函数。
二、库函数
C语言并不提供库函数,C语言的国际标准ANSI规定了一些常用的函数标准,被称为标准库。不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列 函数的实现。这些函数就被称为库函数。
我们学过的scanf、printf都是库函数,库函数也称函数。这些函数是现成的,直接拿来使用即可。用库函数时需要用引用相应的头文件。
举例:
#include<stdio.h>
#include<math.h>
int main()
{
double n = 0;
printf(" %lf \n ",sqrt(n));}
我们要求n的平方根,可以使用sqrt函数,但是使用sqrt函数的时候需要引用头文件“math.h”。
三、自定义函数
函数形式:
ret_type fun_name(形式参数)
{
}
(1)ret_type:表示的是函数的返回值类型,可以是int,也可以void,什么都不返回。
(2)fun_name:表示的是函数名,根据自己的需求起名即可。
(3)( ):括号内放的是形式参数。
(4){ }:大括号括起来的称为函数体。
举例:
接下来我们讲仔细讲解这段代码。
四、形参和实参
1.实际参数
在上面的第13行代码中,调用Add函数时,传递给函数的参数m,n就是实际参数,也称实参。
实际参数就是真实传递给函数的参数。
2.形式参数
在上面的第4行代码中,定义Add函数时,括号里面的x,y就是形式参数,也称形参。
如果只是定义了Add函数,而没有调用,那么参数x和y就是形式上存在的,不会向内存申请空间,不是真实存在的,所以叫形式参数。形式参数只有在函数调用时,用来存放传递过来的实参,才会向内存申请空间,这个过程就是形参的实例化。
3.实参和形参的关系
虽然说实参是传递给形参的,但是他们的是独立的内存空间。
通过调试可以观察到这个现象。虽然x和y得到了m和n的值,但是地址不一样,所以我们可以理解为形参是实参的一份临时拷贝。
五、return语句
1.return后面可以是表达式,也可以是一个数值。如果是表达式,先执行表达式,再返回表达式的结果。
2.return后边也可以什么都没有,这种情况一般适用于返回类型是void的情况。
3.return返回值与函数返回类型不一致,系统会自动将返回的值隐式转换成函数的返回类型。
4.return返回后,函数就彻底返回,后面的语句将不再执行。
5.如果语句中出现if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
六、数组做函数参数
在使用函数解决问题的时候难免会将数组作为参数,传递给函数,在函数内部对数组进行操作。
举例:
#include<stdio.h>
void set_arr(int arr[],int m)
{
for (int i = 0; i < m; i++)
{
arr[i] = -1;
}
}
void print_arr(int arr[], int m)
{
for (int j = 0; j < m; j++)
{
printf("%d ", arr[j]);
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int n = sizeof(arr) / sizeof(arr[0]);
set_arr(arr,n);
print_arr(arr,n);
return 0;
}
七、嵌套调用和链式访问
1.嵌套调用
嵌套调⽤就是函数之间的互相调⽤。
举例:
//假设我们计算某年某⽉有多少天?
#include<stdio.h>
//判断是否是闰年
int is_leap_year(int x)
{
if ((x % 4 == 0 && x % 100 != 0) || x % 400 == 0)
return 1;
else
return 0;
}
//计算这个月有多少天
int get_days_of_month(int x,int y)
{
int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int day = days[y];
if (is_leap_year(x) && y == 2)
day += 1;
return day;
}
int main()
{
int m = 0;
int n = 0;
scanf_s("%d %d", &m, &n);
int r = get_days_of_month(m, n);
printf("%d年%d月有%d天\n", m, n, r);
return 0;
}
这段代码中在get_days_of_month函数中调用了函数is_leap_year,这就属于函数的嵌套调用。
分析一下这段代码:
首先,我们要实现计算某一年的某一月有多少天,我们先要判断这一年是不是闰年(因为闰年的2月有29天),所以我们创建了is_leap_year函数。之后我们要计算这个月有多少天,创建了get_days_of_month函数,其中用数组存储十二个月的天数,再通过if语句判断判断,如果is_leap_year返回1,并且月份是2月,这时天数加1。
2.链式访问
链式访问就是将一个函数的返回值做另一个函数的参数,像链条一样把函数穿起来,就是链式访问。
举例:
#include<stdio.h>
int main()
{
int r = strlen("abcdef"); //求字符串长度
printf("%d\n", r); //打印字符串长度
return 0;
}
这段代码用2条语句完成了动作,如果我们用strlen的返回值直接作为printf的参数,就形成了一条链式访问。代码如下:
#include<stdio.h>
int main()
{
printf("%d\n", strlen("abcdef"));
return 0;
}
八、函数的声明和定义
1.单个文件
在这段代码中,225-231行是这函数的定义,第236行是函数的调用。函数的定义在函数调用之前,这样写没啥问题。但是如果函数定义在函数调用之后呢,代码如下:
这样写,在不同的编译器,可能会发出警告,这时候我们需要在函数调用前对函数进行声明。
函数的定义也是一种特殊的声明,所以函数定义放在函数调用之前也是可以的。我们平时写代码的时候就可以直接把函数定义写在main函数前。
2.多个文件
当我们多人协作写一个项目的时候,不可能所有人在一个.c文件里面进行编辑,这时候就可以创建多个文件。一般函数的声明,类型的声明我们存放在(.h)文件里,函数的实现是放在(.c)源文件里,如下图所示:
3.static和extern
(1)static
static是静态的意思,它可以用来:
①修饰全局变量
②修饰局部变量
③修饰函数
(2)extern
extern是用来声明外部符号的。
(3)作用域和声明周期
作用域:一般来说,一段程序代码中用到的名字不总是有效的,而限定这个名字可用性的代
码范围叫这个名字的作用域。
①局部变量的作用域是:变量所在的局部范围。
②全局变量的作用域是:整个工程。
生命周期:指的是变量的创建(申请内存)到销毁(收回内存)的这一时间段内。
①局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域变量销毁,生命
周期结束。
②全局变量的生命周期是:整个程序的生命周期。
(4)static修饰局部变量:
#include<stdio.h>
void test()
{
int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
test();
}
return 0;
}
这是没有用static修饰局部变量,我们来看他的运行结果。
接下来我们用static修饰局部变量,看看会发生什么变化。
#include<stdio.h>
void test()
{
static int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
test();
}
return 0;
}
通过观察发现:
(1)第一段代码,每次运行,进入test函数都会重新创建i变量,并赋值为0。函数执行完后变量被销毁,所以每次打印i的值都是1。
(2)第二段代码,用了static修饰局部变量i,我们可以看出,i是累加的,并没有销毁,所以重新进入函数也不会重新创建,直接累加上次的值进行计算。
结论:static改变了函数的生命周期。生命周期改变的本质是改变了储存类型,正常局部变量是存储在内存的栈区的,被static修饰后,就存到了静态区。存在静态区的变量和全局变量是一样的,所以生命周期就和程序的生命周期一样了,只有程序结束了,变量才会销毁,但是作用域是不变的。因此,一个变量出了函数后,我们想要保留变量的值,等下次进来继续使用,就可以使用static。
(5)static修饰全局变量:
代码1:
代码2:
extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使用,代码1正常,代码2在编译的时候会出现链接性错误。
结论:⼀个全局变量被static修饰,使得这个全局变量只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤。 本质原因是全局变量默认是具有外部链接属性的,在外部的⽂件中想使⽤,只要适当的声明就可以使⽤;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源 ⽂件内部使⽤了,其他源⽂件,即使声明了,也是⽆法正常使⽤的。因此,如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使⽤ static修饰。