c-programming-chap7
If you have a procedure with 10 parameters, you probably missed some. [1]
[TOC]
函数
引言
函数这个概念源自数学。在其他语言中,函数也叫方法,过程等。所以,编程中函数与数学中的函数是不同的。
我们早在第二章就接触到函数这一概念—— main 函数。当时我煞费苦心的尝试用通俗的话向你们解释 main 函数的构成以及各部分的功能。但我们刚开始学编程,对函数一定不能有一个比较深刻的认识。这一节,我将带领大家走进函数,仔细推敲品味函数。从这一节开始,使用函数的思想将会伴随我们今后的编程生涯。
这一章,会教你如何编写函数,并且更加深入的理解 main 函数本身。
为什么要使用函数
- 一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。较大的程序,可分别放在若干个源文件中。这样便于分别编写和编译,提高调试效率。一个源程序文件可以为多个C程序共用。
- 一个源程序文件由一个或多个函数以及其他有关内容(如指令、数据声明与定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
- C程序的执行是从main函数开始的,如果在main函数中调用其他函数,在调用后流程返回到main函数,在main函数中结束整个程序的运行。
- 所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数间可以互相调用,但不能调用main函数。main函数是被操作系统调用的。
- 从用户使用的角度看,函数有两种。
- 库函数,它是由系统提供的,用户不必自己定义,可直接使用它们。应该说明,不同的C语言编译系统提供的库函数的数量和功能会有一些不同,当然许多基本的函数是共同的。
- 用户自己定义的函数。它是用以解决用户专门需要的函数。
- 从函数的形式看,函数分两类。
- 无参函数。在调用无参函数时,主调函数不向被调用函数传递数据。
- 有参函数。在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据。
函数的定义、声明、调用
在介绍函数的定义之前,让我们先来看 3 个简单定义的函数。
1. 3 个 简单的函数
① 计算平均值
假设计算两个 double 类型的数值的平均值。
1 |
|
② 显示倒计数
不是每一个函数都有返回值:
1 |
|
③ 显示双关语
不是每个函数都有参数:
1 |
|
2. 函数定义
C语言要求,在程序中用到的所有函数,必须 “先定义,后使用”。
定义函数应包括以下几个内容:
- 指定函数的名字,以便以后按名调用。
- 指定函数的类型,即函数返回值的类型。
- 指定函数的参数的名字和类型,以便在调用函数时向它们传递数据。对无参函数不需要这项。
- 指定函数应当完成什么操作,也就是函数是做什么的,即函数的功能。这是最重要的,是在函数体中解决的。
无参函数
1 |
|
有参函数
1 |
|
空函数
1 |
|
函数的返回值
(1) 函数的返回值是通过函数中的return语句获得的
一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个return语句就起作用。return语句后面的括号可以不要,如“return z;”与“return(z);”等价。return后面的值可以是一个表达式。
(2) 函数值的类型
函数值的类型在定义函数时指定。
(3) 在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致
如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。
(4) 对于不带回值的函数,应当用定义函数为“void类型”(或称“空类型”)
这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。此时在函数体中不得出现return语句。
形参(形式参数)
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数)
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。
形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。
每个形式参数前需要写明其类型,形参之间用逗号隔开。
形参实参举例
1 |
|
- 定义函数,名为max,函数类型为int。指定两个形参x和y,形参的类型为int。
- 主函数中包含了一个函数调用max(a,b)。
- max后面括号内的a和b是实参。
- a和b是在main函数中定义的变量,x和y是函数max的形式参数。
- 通过函数调用,在两个函数之间发生数据传递,实参a和b的值传递给形参x和y,在max函数中把x和y中的大者赋给变量z,z的值作为函数值返回main函数,赋给变量c。
在调用函数过程中发生的实参与形参间的数据传递称为“虚实结合”。
3.函数声明
函数声明:(function declaration)在调用前声明每个函数使得编译器可以先对函数进行概要浏览,而函数的定义可以以后再给出。
函数的声明类似函数的第一行:
1 |
|
为 average() 函数添加声明后程序的样子:
1 |
|
我们把这种函数声明称为函数原型(function prototype)。函数原型为如何调用函数提供了完整的描述:返回值类型,实参个数和类型。
4. 函数调用
函数调用的过程
- 在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。在发生函数调用时,函数的形参才被临时分配内存单元。
- 将实参的值传递给对应形参。
- 在执行函数期间,由于形参已经有值,就可以利用形参进行有关的运算。
- 通过return语句将函数值带回到主调函数。应当注意返回值的类型与函数类型一致。如果函数不需要返回值,则不需要return语句。这时函数的类型应定义为void类型。
- 调用结束,形参单元被释放。注意: 实参单元仍保留并维持原值,没有改变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参的值。因为实参与形参是两个不同的存储单元。
函数调用注意事项[重要]
在一个函数中调用另一个函数(即被调用函数)需要具备如下条件:
(1) 首先被调用的函数必须是已经定义的函数(是库函数或用户自己定义的函数)。
(2) 如果使用库函数,应该在本文件开头用#include指令将调用有关库函数时所需用到的信息“包含”到本文件中来。
(3) 如果使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数)的后面(在同一个文件中),应该在主调函数中对被调用的函数作声明(declaration)。 声明的作用是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法。
函数调用示例
函数调用由函数名和实参列表组成,实参列表用圆括号括起来:
1 |
|
程序:判断素数
编写程序提示用户录入数,然后给出一条信息说明此数是否为素数。
1 |
|
把判断素数的实现写到另外一个函数中,此函数返回值为 true 就表示是素数,返回 false 表示不是素数。
参考程序:
1 |
|
main 函数中包含一个叫 n 的变量,is_prime 函数中也有一个叫 n 的变量。这两个变量是虽然同名,但是在内存中的地址不同,是完全不相同的。所以给其中一个变量赋新值不会影响另一个变量。下一章我们还会详细的讨论这个问题。
is_prime 函数中有多条 return 语句。但是任何一次函数调用只能执行其中一条 return 语句,这是因为执行 return 语句后函数就会返回到调用点。本节后面还会深入的学习 return 。
实参形参应用
形式参数:(parameter) 出现再函数的定义中
实际参数:(argument)出现在函数调用中的表达式。
在 C语言中,实际参数是通过值传递的:调用函数时,计算出每个实际参数的值并将它赋值给相应的形式参数。在函数执行的过程中,形式参数的改变不会影响实参的值,这是因为形式参数是实参的副本。从效果上来讲,每个形式参数初始化为相应的实参的值。
1. 数组型实际参数
数组经常被当作实际参数。当形式参数为一维数组时,可以(而且是通常情况下)不说明数组长度:
1 |
|
示例:数组求和
1 |
|
注意1: 把数组名传递给函数时,不要在数组名的后面放置方括号:
1 |
|
注意2: 虽然可以用运算符 sizeof
计算出数组变量的长度,但是它无法给出数组类型的形式参数长度的正确答案:
1 |
|
数组变量作为函数参数的特性 【了解即可】
1) 数组无法检测传入的数组长度是否正确,所以:
-
一个数组有 100 个元素,但是实际仅仅使用 50 个元素,实参可以只写 50:
1
sum_array(a, 50);
函数甚至不会知道数组还有 50 个元素存在!
-
如果实际参数给的比数组还要大,会造成数组越界,从而导致未定义行为
1
sum_array(a, 150);// wrong
2)在函数中改变数组型形式参数的元素,同时会改变实际参数的数组元素。
1 |
|
多维数组
多维数组的形式参数可以省略第一维的长度,比如a[][3]
但是,这样的方式不能传递具有任意列数的多维数组。幸运的是,我们通常可以通过使用指针数组的方式解决这一问题。
局部变量\全局变量
局部变量
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。例如:
1 |
|
全局变量
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。例如:
1 |
|
实例
1. 根据长方体的长宽高求它的体积以及三个面的面积。
根据题意,我们希望借助一个函数得到三个值:体积 v 以及三个面的面积 s1、s2、s3。遗憾的是,C语言中的函数只能有一个返回值,我们只能将其中的一份数据,也就是体积 v 放到返回值中,而将面积 s1、s2、s3 设置为全局变量。全局变量的作用域是整个程序,在函数 vs() 中修改 s1、s2、s3 的值,能够影响到包括 main() 在内的其它函数。
1 |
|
递归
如果一个函数调用它本身,那么此函数就是递归的(recursive)。
有些编程语言极度依赖递归,而有些编程语言甚至不允许使用递归。C语言介于中间:它允许递归,但是大多数 C 程序员并不经常使用递归。
用递归计算 n! 的结果:
1 |
|
为了了解递归的工作原理,一起来追踪下面这个语句的执行:
1 |
|
1 |
|
注意: 要理解 fact 函数最终传递 1 之前,未完成的 fact 函数是如何“堆积”的。在最终传递 1 的那一点上,fact 函数逐个解开,直到 fact(3) 的原始调用返回 6 为止。
上面的程序也可以简化为:
1 |
|
注意: n <= 1
就是终止条件,为了放置无限递归,所有的递归都应该有终止条件。
参考资料:《C语言程序设计:现代方法》
- 如果你写了一个需要10个参数的函数,你或许还漏了什么。Epigrams on Programming 编程警句 ↩
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!