讲一篇通俗易懂的 C 函数。
共 9319字,需浏览 19分钟
·
2022-07-09 10:05
这篇文章是 C 语言系列第三篇,之前两篇见
下面我们来介绍一下 C 语言中一个非常重要的概念 - 函数 (function)。首先就要先给函数下一个定义,函数就是完成特定任务的独立代码单元,这也就是说,一个函数肯定是要为了完成某种功能的,比如一个函数它能够执行加法运算,比如一个函数能交换两个数的值,还有一些函数可能只是为了打印某些东西等等。
函数也可以把很多大的任务拆分成一个个小的任务,通过设计每个小的任务来完成一个大的功能。一个设计优良的函数能够把程序中不需要了解的细节隐藏起来,从而使整个程序结构更加清晰,降低程序的修改难度。
C 语言程序由许多小的函数组成,一个程序会被保存在多个源文件中,每个文件可以单独编译,并可以与库中已编译过的函数一起加载。
下面我们通过一个例子来讨论一下函数是如何创建并使用的。
函数创建以及使用
函数的创建和使用会分为三个步骤:
函数原型 ( function type ):这个是创建函数定义,也叫函数声明,能够表明一个文件中有哪些函数。 函数调用 ( function call ):调用函数的位置,函数被定义出来肯定是要使用它的,在哪里使用的这个函数就被称为函数调用。 函数定义 ( function definition ):这个就是函数的具体要干的什么事儿,也就是函数的具体逻辑是什么。
这么一看,函数和变量简直一模一样了,函数需要原型、调用和定义,而变量也需要这些,只不过变量还可以把原型和定义一起表示。
#include <stdio.h>
int num; // 变量原型
int sum(int,int); // 函数原型
int main(void){
num = 12; // 变量定义
int num2 = 11; // 函数原型 + 函数定义
int all = sum(num,num2); // 变量使用 ,函数使用
printf("all = %d",all);
return 0;
}
// 函数定义
int sum(int a,int b){
return a + b;
}
上面这段代码很好的列举了变量的定义以及函数的定义。
我们首先定义了一个 num 变量,这个就是变量的原型,然后在 main 函数中使用这个变量,就是变量的定义和使用,当然变量也可以直接使用原型 + 定义的方式( 上面的 num2 ),sum 函数演示了函数的原型、定义和使用。这里注意一点,main 函数比较特殊,它是所有方法的入口,而且 main 函数无需定义原型就能直接使用。
上面这段代码被一起保存在一个文件中,当然你也可以把它们保存在不同的文件中,只不过把它们放在同一个文件中我们在演示的时候比较方便,还有一点就是能够一起进行编译,这两个函数也可以定义在不同的文件中,分别进行编译,这样的好处是使程序更加易于维护,代码读起来更加顺畅,事实上项目中也是采用的单独编译的方式。当然你也可以把所有的功能都写在 main 函数中,只不过这样不易于维护,也不符合项目开发标准。
一个完整的函数定义形同如下:
返回值类型 函数名(参数列表)
{
函数体(函数的具体功能)
}
注意我们上面说的只是一个完整的函数定义,而不是每个函数必须都要有返回值类型、参数列表、函数体,只有函数名是必须的(这个肯定好理解)。
当然也有函数定义出来什么都没有做,这就相当于是一个空函数,C 语言默认是允许空函数出现的,比如下面函数就是一个空函数。
sort(){}
sort 函数不执行任何操作也不返回任何值,这种函数可以在程序开发期间用于保留位置,留待以后再填充代码。
程序其实就是一些变量和函数的集合,函数之间的通信可以通过函数参数、返回值来进行,函数通过传递参数,进行一系列的逻辑计算后,把返回值返回回去,以此达到函数交流、通信的目的。
对于函数来说,我们需要了解的两个关键点是参数列表和返回值。
函数参数
对于上面的 sum 函数来说,它的函数参数有两个,分别是 int 类型的 a 和 b,像这种在函数定义的括号中的变量被称为函数参数,这两个变量 a 和 b 也叫做形式参数,简称形参。
和定义在函数中的变量一样,形式参数也是局部变量,这些都属于函数私有的,作用域范围都是从进入函数开始起作用到函数执行完成后作用结束。
当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型,函数原型中你可以使用下面方式定义。
int sum(int a,int b); // 函数原型
也可以省略具体的变量名称,使用下面这种方式进行定义。
int sum(int,int); // 函数原型
在函数原型中没有定义变量,只是声明了两个 int 类型的参数。
除了形参之外,还有一个叫做实际参数 ( 实参 ) 的概念,就对应于上面代码中的 sum(num,num2),因为在调用 sum 的时候是知道 num 和 num2 的具体值的,像这种在调用函数中对参数进行传值的参数被称为实参。
简单点来说就是 形式参数是被调用函数中的变量,实际参数是调用函数赋给被调函数的具体值。实际参数可以是常量、变量,或甚至是更复杂的表达式。
被调函数不知道也不关心传入的数值是来自常量、变量还是一般表达式。实参在把值传递给函数的时候,其实是把值拷贝给被调函数的形式参数,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据。
如下代码所示
#include <stdio.h>
int num; // 变量原型
void sum(int,int); // 函数原型
int main(void){
num = 12; // 变量定义
int num2 = 11; // 函数原型 + 函数定义
sum(num,num2); // 变量使用 ,函数使用
printf("num = %d, num2 = %d",num,num2);
return 0;
}
// 函数定义
void sum(int a,int b){
int sumAll = a + b;
printf("sumAll = %d\n",sumAll);
}
从输出结果可以看出,只要把值传递给 sum 后,不论 sum 函数内部进行何种处理,都不会影响 main 函数中 num 和 num2 的值。
函数返回值
我们上面说过函数之间的通信可以通过函数参数、返回值来进行。函数参数的传递方向是由函数调用者 -> 被调函数,而函数返回值的方向是和参数传递的方向相反,也就是被调函数 -> 函数调用者。
当然并不是所有的函数都需要返回值,而且 return 语句后面也不一定需要表达式,当 return 语句后面没有表达式时,函数不会向调用者返回值。返回值会通过
return 表达式
进行返回,这个返回值的表达式类型和函数定义的返回值类型是一致的。
我们还用上面的 sum 函数来举例子
int sum(int a,int b){
return a + b;
}
可以看到,sum 函数的表达式返回了 a + b,这其实就是一个表达式。而我们可以看到上面的 int main 方法,它的返回值是 0 ,这就是返回了一个常量。
return 后面可以不返回任何值,只是单独写一个 return 也是允许的,不过这种方式相当于没有返回任何值,所以它的函数类型可以定义为 void ,如下代码所示:
// 函数定义
void sum(int a,int b){
int sumAll = a + b;
printf("sumAll = %d\n",sumAll);
return ;
}
使用 return 语句的另外一个作用是终止函数的执行,强制把控制返回给调用函数,如下代码所示:
// 函数定义
int sum(int a,int b){
int sumAll = a + b;
printf("sumAll = %d\n",sumAll);
if(a + b > 0){
return sumAll;
}else{
return 0;
}
}
如果 a + b 的值大于 0 的话,会直接返回 a + b 的和,否则为 0 。这个 if 的控制流程就是强制把结果返回给函数调用者。如果在 if 控制流程后面添加代码的话,那么这段代码不会执行,但是编译却没有给出警告。
在 Java 编辑器中,如果最后一行代码出现在 return 强制返回后面的话,编译器会给出警告或者错误提示这行代码不会被执行。
函数类型
这里需要再强调一下函数类型,定义函数的时候需要声明函数的类型,带返回值的函数类型与返回值类型相同,没有返回值的函数应该将其定义为 void 类型。在老版本的 C 编译器中,如果你没有声明函数类型,编译器会默认把函数当做 int 类型来处理,不过这都是早期的事儿了,现在 C 标准不再支持默认函数为 int 类型这种情况。
在编写函数的时候,你就需要考虑好函数的具体功能是什么,也就是这个函数做了哪些事情,需不需要返回值,如果需要返回值的话,它的返回类型是什么。
函数声明
如果大家学过 Java ,可能对 C 这种先声明再定义的方式很不习惯,为什么函数在定义前需要再单独声明一下呢?我直接定义函数不声明行吗?答案肯定是不行的。
这个先声明再使用一直是 C 语言的标准,标准没有为什么,这就是一个标准,但是这个标准却是一个历史遗留问题。
上世纪 70 年代,大部分计算机内存很小,处理速度也比较差,所以导致代码的运行>时间很长,效率很差,这时候进行我们就需要考虑内存占用和编译时间的问题。因为 C 语言开发的比较早,而且 C 又是和硬件直接打交道的,所以提前声明一下函数能够提前分配内存空间,提升效率。说白了还是效率问题。
还有为什么 C 语言不选择采用预编译一下呢?
参考自 https://www.zhihu.com/question/20567689
首先,C语言出现的很早,那时候编译器也是一个很复杂的东西,当时计算机的内存、外存都很小,编译器做的太大也是一个麻烦的事情,所以事先声明就成为一种规范,保留下来,目的是为了让编译器更简单,虽然这一切已经很过时了。
其次,预编译的成本很高,与脚本语言、解释语言不同,C语言项目的规模可以很大,比如操作系统一级的C语言工程,其源文件有几万个,涉及全局符号几十万个,这样规模的项目预编译一次的负担是很高的,如果是整个项目完全扫描一遍,遍历所有全局符号,再进行真正的编译,估计很多码农都会疯了,等待时间会特别长。
再次,C语言是一种静态链接的语言,如果一个项目被设计成只编译,不链接的方式,比如有些库就会被设计成这样,有些合作开发的项目里,组员之间有时候也只提供obj文件,那么某些全局符号可能就不包含在现有的代码里,那么预搜索就一定找不到某些符号,那么该怎么办?如果不提供声明,这个代码就没办法编译了。
基于以上几点考虑,所以C语言才设计成这样,对于开发者而言,不算友好,但也不算很糟糕,甚至在某些方面是有好处的。
对于一个函数来说,它的最终目的就是通过一系列的逻辑处理获得我们想要的结果,逻辑处理离不开各种程序控制语句,比如说 While 、for、do while 等,下面我们就要来讨论一下这些程序控制语句。
程序控制语句
在有些时候的某些程序可能会重复做一件事情,就应该让计算机做这些重复性的工作,这才是我们需要计算机的意义。毕竟,需要重复计算是使用计算机的主要原因。
C 语言中有很多用于重复计算的方法,我们下面先来介绍其中的一种 --- while 循环。
while 循环
下面我们通过一段代码来看一下 while 循环的使用。
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
printf("%d\n", i);
i++;
}
return 0;
}
这段代码首先声明了一个 i 变量,然后使用了 while 循环来判断 i 的值,当 i 的值 <= 10 的时候,就会执行 while 中的循环逻辑,否则即 i > 10 就会直接跳过循环,不会输出任何结果就直接返回 0 。
如果 i 的值在 10 以内,就会循环打印出来 i 的值。这就是 while 循环的作用。
用通俗易懂的语句来描述 while 循环:当某个判断条件为 true 的时候,循环执行 while 中的代码块。
流程图如下:
在 while 循环中的一个关键点就是进入 while 循环的判断,上面代码就是判断 i <= 10 ,这个表达式是关系运算符的一种。
while循环经常依赖测试表达式作比较,这样的表达式被称为关系表达式,出现在关系表达式中间的运算符叫做关系运算符,下表是我们经常使用到的关系运算符。
运算符 | 说明 |
---|---|
< | 小于 |
<= | 小于或等于 |
== | 等于 |
>= | 大于或等于 |
> | 大于 |
!= | 不等于 |
这些运算符会不单单会出现在 while 循环中,实际上任何逻辑控制语句都会使用到这几种运算符。
这里需要说明一点,不能用关系运算符来比较字符串,比如 ch != '@' 。
虽然关系运算符可以用来比较浮点数,但是要注意:比较浮点数时,尽量只使用 < 和 > 。因为浮点数的舍入误差会导致在逻辑上应该相等的两数却不相等。例如,3乘以1/3的积是1.0。如果用把1/3表示成小数点后面6位数字,乘积则是 .999999,不等于 1。
for 循环
for 循环一个非常明显的特征就是把三个行为组合在一处,也就是初始化、判断、更新,如下代码所示。
#include <stdio.h>
int main()
{
for(int i = 1;i <= 10;i++){
printf("%d\n", i);
}
return 0;
}
可以看到,上面代码中 for 循环分别做了三件事情,每个表达式用 ;
进行分隔。
int i = 0 相当于是对 i 进行初始化操作; i <= 10 相当于对 i 进行一个逻辑判断,逻辑判断是判断是否进行下一次循环的关键。 i++ 相当于是更新 i 的值。
for 循环的一般形式定义如下:
for(表达式1;表达式2;表达式3)
{
语句;
}
这里要注意的是,表达式 1 只在循环开始时执行一次,而表达式 3 是循环结束后再执行。表达式 2 可以省略,省略后默认值为 1,则判断为真,for 循环就会成为一个死循环。
for 循环的流程图如下
do while 循环
一般来说,循环的方式可以分为两种:入口循环和出口循环,什么意思呢?入口循环是先进行循环,再执行每次循环要做的事情,比如上面的 while 循环、for 循环,他们都是先进行判断是否需要进行下一次循环,如果需要的话,才会打印出 i 的值,这就是入口循环。
而出口循环则是要先执行代码,再判断是否要进行下一次循环,即在循环的每次迭代之后检查测试条件,这保证了至少执行循环体中的内容一次,典型的出口循环就是 do ... while。
我们把上面的代码进行修改:
#include <stdio.h>
int main()
{
int i = 1;
do{
printf("%d\n", i);
i++;
}while(i <= 10);
return 0;
}
从输出结果可以看到,do while 循环在执行完循环体后才执行测试条件,所以 do ... while 循环至少执行循环体一次,而 for 循环和 while 循环在执行循环体之前先执行测试条件,do ... while 的一般形式如下
do
代码
while ( 表达式 );
do ... while 循环的流程图如下
到现在为止, C 语言中的程序控制语句我们都了解了,那么该如何进行选择呢?
实际上上面我们已经稍微讨论了一下如何选择的问题了。
while 循环和 for 循环很类似,这两类循环都是先进行一次循环条件的判断,然后再执行具体的循环体操作,只要一次循环条件不满足则一次都不会执行;而 do ... while 循环会至少先进行一次循环,然后才会执行循环判断。
一般来说,使用 for 循环的场景比较多,因为 for 循环形式更加简洁,而且在 for 循环中,变量和判断以及更新的作用域都在循环体内,不会有其他外部代码来修改这些变量,更可控,在 while 和 do ... while 循环中,变量的更新不可控,而且代码也没有 for 循环可读性强。
break 和 continue
break 和 continue 相当于是循环体内领导者的这样一个角色,有了这两个角色存在,循环体内的代码会根据这两个关键字来判断是中断循环还是执行下一次循环。
C 语言中的 break 有两种用法:
一种用法是用在循环体中,当 break 出现在循环体中时,会中断这个循环。 一种用法是用在 switch 语句中,用作中断这个 switch 语句的 case 条件。
break 用于中断循环:如下代码所示
#include <stdio.h>
int main(void)
{
for(int i = 1;i <= 10;i++){
if(i == 5){
break;
}
printf("i 的值 = %d\n",i);
}
return 0;
}
输出的结果是 i 的值 = 1 - 4, 当 i == 5 时,会进入到 if 判断中,if 判断会直接触发 break,break 用于跳出当前循环,当前是 for 循环,所以 break 会直接跳到 for 循环外面,也就是直接 return ,不会再打印 i 的值。
continue 关键字用于跳过当前循环,执行下一次循环,它和 break 很相似但是有着本质的区别,break 是跳出循环,continue 是执行下一次循环,我们同样拿这个代码来说明,只需要把上面的 break 改成 continue 即可。
#include <stdio.h>
int main(void)
{
for(int i = 1;i <= 10;i++){
if(i == 5){
continue;
}
printf("i 的值 = %d\n",i);
}
return 0;
}
(这段代码的输出结果会输出出 i = 5 以外的值)
从输出结果可以看出,只有 i = 5 的值没有输出,这也就是说,当代码执行到 i == 5 的时候,会进行 continue 继续执行当前循环,从而跳过这次循环后面的代码,如下图所示。
总结
这篇文章我主要和你聊了聊 C 语言中的函数,函数定义、函数返回值、参数以及程序控制流程中的三类循环的特点以及选型,最后又介绍了一下 break 和 continue 的作用。
如果文章对你有帮助,还请各位小伙伴们三连支持哦!