前言
在中学时,我们已经接触到了函数的概念。如一次函数:y=kx+b。
其中k和b是常数,只要是给定一个变量x,就可以返回y值。
我们如果称给的x是入口,那么返回的y值即是出口。
同样的,我们还学习过这样的一个函数:y= a;(a为常数)
这样的一个函数,表示一条横着的线。
还是按照入口和出口的说法,那么这时,我们的入口为空,出口为y。
在C语言中,我们经常会遇到一段反复的逻辑需要我们进行处理,在不同的场景下,或许只需要我们切换一下入口即可完成任务。这和我们刚刚解释的函数的概念是相等的,即入口->出口。
因此,聪明的C语言发明者发明了“函数”。
有了函数,我们的开发就会更加的高效,因为一段重复的逻辑只需要写一个函数,下一次直接调用这个函数即可。
在C语言中,我们会遇到两种函数:
- 一种是发明者给我们写好的函数,我们直接调用即可。
- 一种是自己写的函数,需要自己编写。
下面,我们首先来介绍一下发明者给我们写好的函数。
1.标准库:C语言的百宝箱
在我们之前的学习中,和大家说过,scanf和printf以及main这些全部都是函数,这些函数即是C语言的发明者给我们写好的函数。这些函数被存储的地方被称为标准库,这些函数被称为库函数。
2.头文件:头文件的藏宝图
库函数被存储在不同的头文件中,每个头文件都像是一张藏宝图,他告诉了编译器,我们所调用的函数到底在哪。就譬如我们使用的<stdio.h>就是一个头文件。
在我们调用函数时,如果我们没有在前面写了包含的头文件的话,那么我们的编译器就找不到我们的库函数,因此最后会报错。
下面,我们给大家展示一下math库中的sqrt函数。
#include <stdio.h>
int main()
{
printf("%lf", sqrt(16));//sqrt是计算平方根的函数,接受double类型,返回double类型。
}
报错:没有找到sqrt函数的定义。
#include <stdio.h>
#include <math.h>//math.h是包含了sqrt的库,我们用#include包含即可在下面使用sqrt函数。
int main()
{
printf("%lf", sqrt(16));
}
结果:4.000000
下面,给出C语言的文档,大家可以在文档中查阅各种库和各个函数的用法:文档链接
3.自定义函数
我们说过,函数也可以通过自己定义的方式来创建,那么我们下面来介绍函数的定义语法:
函数定义之前需要先声明,声明的作用是告诉编译器有这个函数存在。
函数定义的格式如下:
//函数声明
返回类型 函数名(参数1,参数2,......);
//函数定义
返回类型 函数名(参数1,参数2,......)
{
//函数体
}
通过我们的案例,我们可以自定义如下函数:
int add(int a,int b)
{
return a+b;
}
在上面的这个例子中,我们定义了一个add函数,他接受的两个值是整型的,并返回了他们的和,因此,我们可以使用这个函数计算两数之和。
注意:
- 我们函数名前面是返回类型,如果我们不需要返回一个值的话,那么我们写void即可(空),即返回空
- 我们返回,是把一个值(值1)返回给调用函数的值(值2),因此,我们如果需要返回的话,是需要有一个值来接收这个返回值的。
需要注意的是,我们即便不写函数声明也不会报错,但是这样子虽然可以少写一行代码,但是缺点也很明显,我们做出如下的解释:
没有声明,编译器就不知道有这个函数,而我们编译又是一行一行编译的,如果出现下面的情况:
int ThreeNumberAdd(int a,int b,int c)
{
int d=add(a,b);
int e=add(c,d);
return e;
}
int add(int a,int b)
{
return a+b;
}
那么这时,我们会发现报出了如下警告:
因此,这种方式是不可取的,我们还是要写上函数声明为妙。
4.自己的头文件
之前我们介绍过,库函数在头文件中,我们也可以写自己的头文件,在vs2022中,可以通过新建一个头文件来解决这个问题。
步骤1:
步骤2:
这样,我们就可以创建出自己的头文件了。
在我们的实际工程中,我们将函数声明放进头文件内,并包含在.c文件中,如下:
注意点:自己写的头文件要用双引号包含。
5.形参和实参
5.1形参和实参的概念
在上⾯代码中,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简 称形参。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x
和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x+y;
return z;
}
int main()
{
int a = 0;
int b = 0;
//输⼊
scanf("%d %d", &a, &b);
//调⽤加法函数,完成a和b的相加
//求和的结果放在r中
int r = Add(a, b);
//输出
printf("%d\n", r);
return 0;
}
在上⾯代码中,第2~7⾏是 Add 函数的定义,有了函数后,再第17⾏调⽤Add函数的。
我们把第17⾏调⽤Add函数时,传递给函数的参数a和b,称为实际参数,简称实参。
实际参数就是真实传递给函数的参数。
5.2形参和实参的关系
实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x
和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在
函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。
虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各⾃是独⽴的内存空
间。
下面,我们通过调试的监视窗口帮助大家理解这句话:
因此,实参和形参其实不是同一个变量。我们可以将形参理解为实参的临时拷贝。
在这里,我们再给出大家使用return语句的一些注意事项:
- return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式的结果。
- return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
- return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
- return语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
- 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
6.数组传参
在使用函数的时候,难免会将数组作为参数传递给函数,然后在函数的内部对数组进行操作,现在我们给出数组形参的写法:
#include <stdio.h>
void set_arr(int arr2[10], int sz2)
{
int i = 0;
for (i = 0; i < sz2; i++)
{
arr2[i] = -1;
}
}
void print_arr(int arr2[], int sz2)//我们可以不写方括号里面的数字。
{
int i = 0;
for (i = 0; i < sz2; i++)
{
printf("%d ", arr2[i]);
}
printf("\n");
}
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
set_arr();//设置数组内容为-1
print_arr();//打印数组内容
return 0;
}
7.函数栈帧的简单理解
懒得写了,自己去听视频吧。
8.函数中定义静态变量
//代码1
#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
}
//代码2
#include <stdio.h>
void test()
{
//static修饰局部变量
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是每次进⼊test函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。
代码2中,我们从输出结果来看,i的值有累加的效果,其实 test函数中的i创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。
结论:在函数中定义的静态变量,在函数执行结束之后不会被销毁。