【6.0】C-函数
前言
函数是组成C语言的基本单位,为了提高程序设计的质量和效率,C系统提高了大量的标准函数。例如前面部分提到的printf()
,scanf()
等函数。同样的,我们可以根据我们的实际需求来定义我们的函数。
【关于Function
(函数)翻译问题】
如果你学过或者接触过现代的一些计算机语言,获取你会了解到一个词——“方法”。不论是函数也好,方法也罢,都是出自这个次——Function
,就个人经验而言,这是个翻译的历史遗留问题,早起计算机进入我国的时候,那个时候没有相关的经验和相关书籍的参考,再加上一开始的计算机是数学领域发展起来的,就通俗的直译为——函数,由于现代计算机的发展和大量的经验,对于程序员来说,函数这个词非常的抽象,没有办法形成——望文知意,经过本土化,翻译为方法更复合实际,所以你如果接触过相对现代一些的教程都是说是方法而不是函数。
当然还有另一个说法,是对于面向过程编程语言,称为函数,而面向对象编程语言,称为方法。
(PS:面向对象编程是在面向对象过程编程的基础上发展而来的)
以上两种解释没有谁对谁错,此处仅做说明,以防初学者出现歧义。
函数概述
函数的概念
在前面的所有示例中,一个程序中只有一个main()
函数。对于复杂的程序,如果只有一个main()
函数,将会影响程序的可读性,也不能体现程序的结构化设计的思想。因此,需要将某种特定功能的代码定义为函数,一个程序由main()
函数和若干函数组成,每个函数在程序中形成即相对独立又相互联系的模块。**main()
函数可以调用其他函数,其他函数也可以相互调用**。
一个简单的函数代码示例:
【实例】实现两数加法运算。
【代码示例】
1 |
|
库函数
从用户使用的角度来说,C语言的函数可以分为库函数和用户自定义函数。库函数是系统提供的,用户不必自己定义而可以直接使用。库函数由系统预定义在相应的文件中,使用时需要在程序的开头把该函数所在的头文件包含进来。例如,为了调用printf()
,scanf()
函数,需要调用#include <stdio.h>
包含stdio.h
头文件;为了调用sqrt()
,log()
函数,需要调用#include <math.h>
包含math.h
头文件。
使用库函数需要注意以下几个问题:
- 函数的功能
- 函数参数的数目和顺序,以及每个参数的意义以及类型
- 函数的返回值的意义以及类型
- 需要使用的包含文件
常用的标准函数库参考C 标准库
有关问题底层大全等可以查询微软文档。
用户自定义函数
函数定义的格式
函数由函数名,形参列表和函数体组成。函数名是用户为函数起的名字,用来标识唯一一个函数;函数的形参列表用来接受调用函数传递的数据,形参列表可以为空,此时函数名后的括号不可以省略;函数体是函数实现自身功能的一组语句。
无参数函数的定义格式
1
2
3
4类型声明符 函数名称()
{
//函数体
}**类型声明符指定函数值的类型,即函数返回值的类型。如果一个函数没有返回值,该函数的返回值类型为
void
**。函数名称的命名规则与变量的名称规则相同。有参数函数的定义格式
1
2
3类型声明符 函数名称(形参列表声明){
//函数体
}其中,类型声明符号指定函数返回值的类型,可以是任何有效类型,如果省略类型声明符号,系统默认函数的返回值为
int
类型。如果函数只是执行相关操作而不需要返回值,则可以使用void
。有参函数在函数名称后的括号内必须有形式参数表,用于调用函数和被调用函数之间的数据传递,故必须对其进行类型声明,这由形式参数声明部分完成。一般情况下,函数执行需要多少原始外部数据,就有多少个形参数据,形参之间用逗号隔开。代码示例:
1
2
3int Plus(int a,int b){ //定义自定义函数
return a + b; //返回值
}空函数
C语言中可以有空函数,代码示例:
1
2
3类型声明符 函数名()\
{
}调用该函数,什么工作都不做。在主函数调用该函数,可以暂时表示某功能,后期扩充的时候填充该函数。
形式参数和实际参数
在调用有参函数时,主调函数和被调函数之间往往有数据传递关系。在定义函数时函数名后面小括号内的变量为形式参数(简称形参),函数调用时用于接收主调函数传来的数据。在调用函数时,主调函数的函数调用语句的函数名后面小括号的参数称为实际参数(简称实参)。
【实例】编写函数求三个整数中的最小值
【代码示例】
1 |
|
【输出】
【实例】求三个实数的平均值
【代码示例】
1 |
|
【输出】
如果你把形参的类型改为
int
类型,则返回的值为4
函数的返回值
通常是系统通过函数调用使主函数从被调函数得到一个确定的值,这就是函数的返回值。在C语言中,是通过return
语句来实现的。return
语句一般有以下3种形式:
1 | return 表达式; |
需要注意的是:
return
语句有双重作用:它使得函数从被调函数中退出,返回到调用的代码处,并向调用函数返回一个确定的值。- 一个函数中可以用有多个
return
语句,执行到哪一个return
语句,哪个return
语句就起作用。
函数的调用
所谓函数的调用,是指一个函数(主调函数)暂时中断本函数的运行,转去执行另一个函数(被调函数)的过程。被调函数执行完成后,返回调用函数中断处继续调用函数的运行,则是一个返回过程。函数的一次调用必定伴随着一个返回过程,在函数的调用和返回这个过程中,两个函数之间发生信息的交换。
函数调用的一般形式
语法格式如下:
1 | 函数名(参数列表); |
说明:
- 如果调用无参函数,则实参列表可以没有,但是括号不能省略。
- 实参列表的参数类型和个数必须与形参相同且顺序一致,多个实参之间用逗号隔开。
函数的调用方式
按照被调用函数在主调函数中出现的位置和完成的功能划分,函数调用有如下方式:
把函数调用作为一个语句。此时一般不需要返回值,只需要执行特定的操作。
在表达式中调用函数,这种表达式称为函数表达式。此时要求函数返回一个值参与运算,例如:
1
a=c*Plus(a,b,c);
将函数调用作为另一个函数调用的参数。例如:
1
printf("和为:%d",Sum(a,b));
【实例】编写函数判断一个数是否是素数
【代码示例】
1 |
|
【输出】
函数的调用过程
代码示例如上面的示例,图示如下:
函数的原型声明
与变量的定义和使用一样,函数的调用也要遵循“先声明,后调用”的原则。在一个函数调用另一个函数时,需要具备以下条件:
被调函数必须已经存在。
如果使用库函数,需要提前引用相关库函数。
如果使用用户自定义函数,并且该函数与主调函数在同一个文件中,这时被调用函数应该放在主调函数之前定义。如果函数调用的位置在函数定义之前,则在函数调用之前必须对所调用的函数进行函数原型声明,函数原型声明的语法格式如下:
1
类型声明符 函数名(形参表);
函数原型声明是向编译器表示一个函数的名称,将接收什么样的参数和有什么样的返回值,使编译器能够检查函数调用的合法性。实际上就是函数定义时的函数头,最后加上分号构成的声明语句。与函数头的区别是,函数声明中的形参列表中可以只写类型名,而不写形参名。代码示例:
1 | float average(float x,float y,float z); |
函数的参数传递
在C语言中进行函数调用时,有两种不同的参数传递方式:值传递和地址传递。
值传递
在函数调用时,实参将其值传递给形参。这种传递方式即为值传递。
C语言规定,实参对形参的数据传递是值传递,即单向传递,也就是只能由实参传递给形参而不能由形参传递回来给实参。这是因为,在内存中,实参和形参占用不同的存储单元。在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参的存储单元会被释放,实参的存储单元仍要保留维持原值。因此,在执行一个被调用函数时,形参的变化不会改变实参的值。
地址传递
地址传递指的是函数调用时,实参将某些量的地址传递给形参。这样实参和形参指向同一个内存空间,在执行被调函数的过程中,对形参所指向的空间中的内容改变能够直接影响到实参的值。
在地址传递方式下,形参和实参可以是指针变量(欠-指针)
函数的嵌套调用和递归调用
函数的嵌套调用
C语言中的函数的定义是相互平行的,在定义函数时,一个函数不能包含另一个函数。但是,一个函数在被调用的过程中可以调用其他函数,这就是函数的嵌套调用。
如下为常规的嵌套调用图示:
【实例】计算1+$2!$+$3!$+·····+$10!$。(使用嵌套函数)
【代码示例】
1 |
|
【输出】
函数的递归调用
在调用一个函数的过程中又直接或者间接的调用该函数本身,称为函数的递归调用。
递归是一种非常实用的程序设计技术。许多问题具有递归的特性,在某些情况下,用其他方法很难解决的问题,利用递归可以轻松解决。
【实例】利用递归安抚计算$n!$。
【代码示例】
1 |
|
【输出】
数组作为函数的参数
关于函数的形参前面说明过支持任何类型的参数,同样的数组也是支持的。
一维数组作为函数参数
用数组名作为函数实参时,向形参传递的是数组的地址值。在定义数组形参时,只需要在后面跟一个方括号就可以。
【实例】求一组整数的平均值(一维数组)
【代码示例】
1 |
|
【输出】
二维数组作为函数的参数
可以使用二维数组名作为函数参数,此时的实参可以直接使用二维数组名,在被调函数中可以指定形参所有维数的大小,也可以省略一维大小的声明。例如:
1 | void find(char x[3][10]); |
这两个声明都是合法的,但是不能把第二维或者更高维度的大小省略,例如下面的定义说不合法的:
1 | void find(char x[][]); |
在第二维相同的情况下,形参数组的第一维可以与实参数组不同,例如:
1 | //实参为: |
以上两种形参定义都是可以的,这是形参数组和实参数组都是由相同类型的一维数组组成的,C语言系统不检查第一维度的大小。
【实例】实现两个 3 $\times$ 4 矩阵A和B的加法运算。
【代码示例】
1 |
|
【输出】
局部变量和全局变量
C语言程序是由一些函数组成的。每个函数都是相对独立的代码块,这些代码只局限于该函数。因此,在非特殊说明下,一个函数的代码对于程序的其他部分来说是隐藏的,它既不会影响程序的其他部分,也不会受程序其他部分的影响。也就是说一个函数的代码和数据不可能与另一个函数的代码和数据相互作用。这是因为它们分别有着自己的作用域。根据作用域的不同,变量分为两种类型:局部变量和全局变量。
局部变量
在函数内部定义的变量称为局部变量。局部变量的作用域仅局限于定义它的函数中。例如:
说明:
- 主函数
main()
中定义的变量也是局部变量,仅在main()
函数中有效。 - 形参也是局部变量,只能在定义它的函数中有效。
- 不同的函数中,可以使用相同名称的局部变量,它们代表不同的对象,互不干扰。
【实例】判断下述代码的运行结果
【代码示例】
1 |
|
【输出】
全局变量
在函数体外定义的变量称为全局变量,全局变量的作用域是从它的定义点开始到文件结束,即位于全局变量定义后面的所有函数都可以使用该变量。
说明:
如果要在定义全局变量之前使用函数中的使用该变量,则需要在该函数中使用关键词extern
对全局变量进行外部声明。
【实例】extern
关键字使用
【代码示例】
1 |
|
【输出】 a=1 b=2
【实例】全局变量与局部变量同名,分析结果
【代码示例】
1 |
|
【输出】
利用全局变量可以减少函数实参的个数,从而减少内存空间以及传递数据时的时间消耗。但是一般还是建议除非必要,尽量不要使用全局变量,因为:
- 全局变量使函数的执行依赖于外部变量,降低了程序的通用性。模块化程序设计要求各个模块之间的“关联性”应尽量的小,函数尽可能是封闭的,只通过参数与外界发生联系。
- 降低程序的清晰性。
- 全局变量在整个程序的执行过程中都会占用存储空间。
变量的存储类别
从变量的作用域,即空间的角度看,变量分为局部变量和全局变量。
从变量的生存期,即变量的存在时间看,变量可以分为静态变量和动态变量。静态变量和动态变量是按照其存储方式来区分的。静态存储方式是指在程序运行期间分配固定的存储空间,程序执行完毕才释放。动态存储方式是在程序运行期间根据需要动态的分配存储空间,一旦动态过程结束,不论程序是否结束,都将释放存储空间。
在C语言中,供用户使用的存储空间分为三部分,即程序区,静态存储区和动态存储区。
- 程序区:存放用户程序。
- 静态存储区:存放全局变量,静态局部变量和外部变量。
- 动态存储区:存放局部变量,函数形参变量。
CPU寄存器存放寄存器变量
【关于静态局部变量的作用】
我在看到静态局部变量的时候,第一反应是这个有什么用?局部使用使用完成都不再使用,为何使用静态来占用内存,目前我大概理解两种静态局部变量的适用情况:
变量如果没有赋初值,对于一般的变量其值系统给的是
NULL
,静态局部变量系统会赋予初始值0;静态局部变量占用内存,其值会一直保存,可以适用于需要多次调用某方法,但是值要增加的条件等,代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(){
int Plus();
for (int i = 0; i < 3; i++)
{
printf("主函数变量%d", Plus()); //调用三次,获得三次不同的结果
}
return 0;
}
int Plus(){
static int a; //静态局部变量
a++; //自增
return a; //返回结果
}输出结果:主函数变量1 主函数变量2 主函数变量3
详细相关静态局部变量的内容查看局部变量的存储类型-静态局部变量
C语言有4种变量存储类别声明符,用来通知编译程序采用哪种方式存储变量,这4种变量存储类别声明符是:
- 自动变量声明符
auto
(一般省略) - 静态变量声明符
static
- 外部变量声明符
extern
- 寄存器变量声明符
register
局部变量的存储类型
局部变量可有3种存储类型:自动变量,局部静态变量和寄存器变量。
自动变量
自动变量是C语言中使用最多的一种变量。因为建立和释放这种类型的变量,都是系统自动进行的,所以称为自动变量。声明自动变量的语法格式如下:
1 | [auto] 类型声明符 变量名; |
其中,auto
是自动变量存储类别声明符,一般可以省略。如果省略,系统默认该变量为auto
。例如:
1 | auto int a; |
自动变量是在动态存储区分配存储单元的。在一个函数中定义自动变量,在调用次函数时才会给变量分配存储空间,当函数执行完毕,这些单元被释放,自动变量中存放的数据也随之丢失。每次调用函数,自动变量重新被赋值,且其默认初值是不确定的。
局部静态变量
如果希望在函数调用结束后仍然保留其中定义的局部变量的值,则可以将局部变量定义为局部静态变量。声明局部静态变量的语法格式如下:
1 | static 类型声明符 变量名; |
说明:
- 局部静态变量是在静态存储区分配存储单元的。一个变量被声明为静态,在编译时即分配存储空间,在整个程序运行期间都不释放。因此,函数调用结束后,它的值并不消失,其值能够保持连续性。
- 局部静态变量是在编译过程中赋予初值的,且只赋予一次初值,在程序运行时其初值已定,以后每次调用函数时,都不再赋予初值,而是保留上一次函数调用结束时的结果。
- 局部静态变量在未显式初始化时,编译系统把它们初始化为
0
(整型变量),0.0
(实型变量),\0(空字符)
(字符型变量)。
寄存器变量
寄存器变量具有与自动变量完全相同的性质。当把一个变量指定为寄存器存储类型时,系统将它们放在CPU中的一个寄存器中,通常把使用频率较高的变量(如循环次数较多的循环变量)定义为register
类型。
【实例】寄存器变量的应用
【代码示例】
1 |
|
【输出】
说明:
- 只有局部自动变量和形参可以作为寄存器变量,其他(如全局变量,局部静态变量则不行)。
- 只有
int
,char
和指针类型变量可以定义为寄存器类型,而long
,double
和float
型变量不能设定为寄存器类型,因为它们的数据长度已经超出通用寄存器本身的位长。 - 可用于变量空间分配的寄存器的个数依赖于具体的机器
全局变量的存储类别
全局变量是在静态存储区域分配单元的,其默认值初值为0。全局变量的存储类型有两种,即外部extern
类型和静态static
类型。
外部全局变量
在多个源程序文件的情况下,如果在一个文件中要引用其他文件中定义的全局变量,则应该在需要引用变量的文件中,使用extern
进行该变量的声明。
【实例】调用其他文件的全局变量
【代码示例】
1 | //文件一代码 |
【输出】32
说明:
extern
只能用来声明变量,不能用来定义变量。因为它不会生成新的变量,只是表示该变量已在其他地方有过定义。extern
用来声明变量时,类型名可以写,也可以不写,例如:1
2
3extern int a;
//或者
extern a;extern
不能用来初始化变量,例如:1
extern int a=1;
静态全局变量
在程序设计时,如果希望在一个文件中定义的全局变量仅限于被本文件引用,而不能被其他文件访问,则可以在定义次全局变量前面加上static
关键词,例如:
1 | static int a; |
此时,全局变量的作用域仅限于本文件内,其他文件中即使进行了extern
声明,也无法使用该变量。
由此可见,静态全局变量与外部全局变量在同一个文件的作用域是一样的,但是外部全局变量的作用域可以延伸至其他程序文件,而静态全局变量在被定义的文件以外是不可见的。
内部函数和外部函数
C语言由函数组成的,这些函数既可以在一个文件中也可以在多个不同的文件中,根据函数的使用范围,可以将其分为内部函数和外部函数。
内部函数
使用存储类别static
定义的函数称为内部函数,其一般形式:
1 | static 类型声明符 函数名(形参表); |
内部函数又称为静态函数。内部函数只能被本文件中的其他函数所调用,而不能被其他外部文件调用。使用内部函数,可以使函数局限于所在文件,如果在不同的文件中有同名函数,则互不干扰。
外部函数
使用存储类别extern
(或者没有指定存储类别)定义的函数,其作用域是整个程序的各个文件,可以被其他文件的任何函数调用,称为外部函数。一般函数没有指定存储类别,都是外部函数。语法格式如下:
1 | extern 类型声明符 函数名(形参表); |
由于函数都是外部性质的,因此,在定义函数时,关键字extern
可以省略。
在调用函数的文件中,一般要用extern
声明所用的函数是外部函数。