C语言函数学习介绍-函数概述

文章logo

一.函数概述

C源程序是由函数组成的。虽然在前面各章的程序中大都只有一个主函数main(),但实用程序往往由多个函数组成。函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能。C语言中的函数相当于其它高级语言的子程序。C语言不仅提供了极为丰富的库函数(如Turbo C,MS C都提供了三百多个库函数),还允许用户建立自己定义的函数。用户可把自己的算法编成一个个相对独立的函数模块,然后用调用的方法来使用函数。可以说C程序的全部工作都是由各式各样的函数完成的,所以也把C语言称为函数式语言。

由于采用了函数模块式的结构,C语言易于实现结构化程序设计。使程序的层次结构清晰,便于程序的编写、阅读、调试。

在C语言中可从不同的角度对函数分类。

1. 从函数定义的角度看,函数可分为库函数和用户定义函数两种。

1) 库函数:由C系统提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数原型的头文件即可在程序中直接调用。在前面各章的例题中反复用到printf、scanf、getchar、putchar、gets、puts、strcat等函数均属此类。

2) 用户定义函数:由用户按需要写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用。

2. C语言的函数兼有其它语言中的函数和过程两种功能,从这个角度看,又可把函数分为有返回值函数和无返回值函数两种。

1) 有返回值函数:此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。如数学函数即属于此类函数。由用户定义的这种要返回函数值的函数,必须在函数定义和函数说明中明确返回值的类型。

2) 无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。这类函数类似于其它语言的过程。由于函数无须返回值,用户在定义此类函数时可指定它的返回为“空类型”, 空类型的说明符为“void”。

3. 从主调函数和被调函数之间数据传送的角度看又可分为无参函数和有参函数两种。

1) 无参函数:函数定义、函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送。此类函数通常用来完成一组指定的功能,可以返回或不返回函数值。

2) 有参函数:也称为带参函数。在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在函数调用时也必须给出参数,称为实际参数(简称为实参)。进行函数调用时,主调函数将把实参的值传送给形参,供被调函数使用。

4. C语言提供了极为丰富的库函数,这些库函数又可从功能角度作以下分类。

1) 字符类型分类函数:用于对字符按ASCII码分类:字母,数字,控制字符,分隔符,大小写字母等。

2) 转换函数:用于字符或字符串的转换;在字符量和各类数字量(整型,实型等)之间进行转换;在大、小写之间进行转换。

3) 目录路径函数:用于文件目录和路径操作。

4) 诊断函数:用于内部错误检测。

5) 图形函数:用于屏幕管理和各种图形功能。

6) 输入输出函数:用于完成输入输出功能。

7) 接口函数:用于与DOS,BIOS和硬件的接口。

8) 字符串函数:用于字符串操作和处理。

9) 内存管理函数:用于内存管理。

10) 数学函数:用于数学函数计算。

11) 日期和时间函数:用于日期,时间转换操作。

12) 进程控制函数:用于进程管理和控制。

13) 其它函数:用于其它各种功能。

以上各类函数不仅数量多,而且有的还需要硬件知识才会使用,因此要想全部掌握则需要一个较长的学习过程。应首先掌握一些最基本、最常用的函数,再逐步深入。由于课时关系,我们只介绍了很少一部分库函数,其余部分读者可根据需要查阅有关手册。

还应该指出的是,在C语言中,所有的函数定义,包括主函数main在内,都是平行的。也就是说,在一个函数的函数体内,不能再定义另一个函数,即不能嵌套定义。但是函数之间允许相互调用,也允许嵌套调用。习惯上把调用者称为主调函数。函数还可以自己调用自己,称为递归调用。

main 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从main函数开始,完成对其它函数的调用后再返回到main函数,最后由main函数结束整个程序。一个C源程序必须有,也只能有一个主函数main。

C语言中的函数,从9个方面透彻分析

首先,什么是函数?函数(function)是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同,但是细节上略有不同。一些函数执行某些动作,如printf()把数据打印到屏幕上;一些函数找出一个值供程序使用,如strlen()把指定字符串的长度返回给程序。一般而言,函数可以同时具备以上两种功能。

为什么要使用函数?首先,使用函数可以省去编写重复代码的苦差。如果程序要多次完成某项任务,那么只需编写一个合适的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序中使用putchar()一样。其次,即使程序只完成某项任务一次,也值得使用函数。因为函数让程序更加模块化,从而提高了程序代码的可读性,更方便后期修改、完善。例如,假设要编写一个程序完成以下任务:

  • 读入一系列数字;
  • 分类这些数字;
  • 找出这些数字的平均值;
  • 打印一份柱状图。

可以使用下面的程序:

当然,还要编写4个函数readlist()、sort()、average()和bargraph()的实现细节。描述性的函数名能清楚地表达函数的用途和组织结构。然后,单独设计和测试每个函数,直到函数都能正常完成任务。

如果这些函数够通用,还可以用于其他程序。许多程序员喜欢把函数看作是根据传入信息(输入)及其生成的值或响应的动作(输出)来定义的“黑盒”。如果不是自己编写函数,根本不用关心黑盒的内部行为。例如,使用printf()时,只需知道给该函数传入格式字符串或一些参数以及printf()生成的输出,无需了解printf()的内部代码。以这种方式看待函数有助于把注意力集中在程序的整体设计,而不是函数的实现细节上。因此,在动手编写代码之前,仔细考虑一下函数应该完成什么任务,以及函数和程序整体的关系。

如何了解函数?首先要知道如何正确地定义函数、如何调用函数和如何建立函数间的通信。我们从一个简单的程序示例开始,帮助读者理清这些内容,然后再详细讲解。

我们的第1个目标是创建一个在一行打印40个星号的函数,并在一个打印表头的程序中使用该函数。如程序清单9.1所示,该程序由main()和starbar()组成。

该程序的输出如下:

该程序要注意以下几点。

  • 程序在3处使用了starbar标识符:函数原型(functionprototype)告诉编译器函数starbar()的类型;函数调用(function call)表明在此处执行函数;函数定义(function definition)明确地指定了函数要做什么。
  • 函数和变量一样,有多种类型。任何程序在使用函数之前都要声明该函数的类型。因此,在main()函数定义的前面出现了下面的ANSI C风格的函数原型:

圆括号表明starbar是一个函数名。第1个void是函数类型,void类型表明函数没有返回值。第2个void(在圆括号中)表明该函数不带参数。分号表明这是在声明函数,不是定义函数。也就是说,这行声明了程序将使用一个名为starbar()、没有返回值、没有参数的函数,并告诉编译器在别处查找该函数的定义。对于不识别ANSI C风格原型的编译器,只需声明函数的类型,如下所示:

注意,一些老版本的编译器甚至连void都识别不了。如果使用这种编译器,就要把没有返回值的函数声明为int类型。当然,最好还是换一个新的编译器。

– 一般而言,函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名(signature)。对于starbar()函数而言,其签名是该函数没有返回值,没有参数。

– 程序把starbar()原型置于main()的前面。当然,也可以放在main()里面的声明变量处。放在哪个位置都可以。

– 在main()中,执行到下面的语句时调用了starbar()函数:

这是调用void类型函数的一种形式。当计算机执行到starbar();语句时,会找到该函数的定义并执行其中的内容。执行完starbar()中的代码后,计算机返回主调函数(calling function)继续执行下一行(本例中,主调函数是main()),见图9.1(更确切地说,编译器把C程序翻译成执行以上操作的机器语言代码)。

– 程序中starbar()和main()的定义形式相同。首先函数头包括函数类型、函数名和圆括号,接着是左花括号、变量声明、函数表达式语句,最后以右花括号结束。注意,函数头中的starbar()后面没有分号,告诉编译器这是定义starbar(),而不是调用函数或声明函数原型。

Structure of a simple function.

  • 程序把starbar()和main()放在一个文件中。当然,也可以把它们分别放在两个文件中。把函数都放在一个文件中的单文件形式比较容易编译,而使用多个文件方便在不同的程序中使用同一个函数。如果把函数放在一个单独的文件中,要把#define和#include指令也放入该文件。我们稍后会讨论使用多个文件的情况。现在,先把所有的函数都放在一个文件中。main()的右花括号告诉编译器该函数结束的位置,后面的starbar()函数头告诉编译器starbar()是一个函数。
  • starbar()函数中的变量count是局部变量(localvariable),意思是该变量只属于starbar()函数。可以在程序中的其他地方(包括main()中)使用count,这不会引起名称冲突,它们是同名的不同变量。

如果把starbar()看作是一个黑盒,那么它的行为是打印一行星号。不用给该函数提供任何输入,因为调用它不需要其他信息。而且,它没有返回值,所以也不给main()提供(或返回)任何信息。简而言之,starbar()不需要与主调函数通信。接下来介绍一个函数间需要通信的例子。

上述程序的输出中,如果文字能居中,信头会更加美观。可以通过在打印文字之前打印一定数量的空格来实现,这和打印一定数量的星号(starbar()函数)类似,只不过现在要打印的是一定数量的空格。虽然这是两个任务,但是任务非常相似,与其分别为它们编写一个函数,不如写一个更通用的函数,可以在两种情况下使用。我们设计一个新的函数show_n_char()(显示一个字符n次)。唯一要改变的是使用内置的值来显示字符和重复的次数,show_n_char()将使用函数参数来传递这些值。

我们来具体分析。假设可用的空间是40个字符宽。调用show_n_char(\’*\’, 40)应该正好打印一行40个星号,就像starbar()之前做的那样。第2行GIGATHINK, INT.的空格怎么处理?GIGATHINK, INT.是15个字符宽,所以第1个版本中,文字后面有25个空格。为了让文字居中,文字的左侧应该有12个空格,右侧有13个空格。因此,可以调用show_n_char(\’ \’,12)。

show_n_char()与starbar()很相似,但是show_n_char()带有参数。从功能上看,前者不会添加换行符,而后者会,因为show_n_char()要把空格和文本打印成一行。程序清单9.2是修改后的版本。为强调参数的工作原理,程序使用了不同的参数形式。

该函数的运行结果如下:

****************************************

GIGATHINK, INC.

101 Megabuck Plaza

Megapolis, CA 94904

****************************************

下面我们回顾一下如何编写一个带参数的函数,然后介绍这种函数的用法。

函数定义从下面的ANSI C风格的函数头开始:

该行告知编译器show_n_char()使用两个参数ch和num,ch是char类型,num是int类型。这两个变量被称为形式参数(formal-argument,但是最近的标准推荐使用formalparameter),简称形参。和定义在函数中变量一样,形式参数也是局部变量,属该函数私有。这意味着在其他函数中使用同名变量不会引起名称冲突。每次调用函数,就会给这些变量赋值。

注意,ANSI C要求在每个变量前都声明其类型。也就是说,不能像普通变量声明那样使用同一类型的变量列表:

ANSI C也接受ANSI C之前的形式,但是将其视为废弃不用的形式:

这里,圆括号中只有参数名列表,而参数的类型在后面声明。注意,普通的局部变量在左花括号之后声明,而上面的变量在函数左花括号之前声明。如果变量是同一类型,这种形式可以用逗号分隔变量名列表,如下所示:

当前的标准正逐渐淘汰ANSI之前的形式。读者应对此有所了解,以便能看懂以前编写的程序,但是自己编写程序时应使用现在的标准形式(C99和C11标准继续警告这些过时的用法即将被淘汰)。虽然show_n_char()接受来自main()的值,但是它没有返回值。因此,show_n_char()的类型是void。下面,我们来学习如何使用函数。

在使用函数之前,要用ANSI C形式声明函数原型:

当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。根据个人喜好,你也可以省略变量名:

在原型中使用变量名并没有实际创建变量,char仅代表了一个char类型的变量,以此类推。

再次提醒,ANSI C也接受过去的声明函数形式,即圆括号内没有参数列表:

这种形式最终会从标准中剔除。即使没有被剔除,现在函数原型的设计也更有优势(稍后会介绍)。了解这种形式的写法是为了以后读得懂以前写的代码。

在函数调用中,实际参数(actual argument,简称实参)提供了ch和num的值。考虑程序清单9.2中第1次调用show_n_char():

实际参数是空格字符和12。这两个值被赋给show_n_char()中相应的形式参数:变量ch和num。简而言之,形式参数是被调函数(called-function)中的变量,实际参数是主调函数(calling-function)赋给被调函数的具体值。如上例所示,实际参数可以是常量、变量,或甚至是更复杂的表达式。无论实际参数是何种形式都要被求值,然后该值被拷贝给被调函数相应的形式参数。以程序清单9.2中最后一次调用show_n_char()为例:

构成该函数第2个实际参数的是一个很长的表达式,对该表达式求值为10。然后,10被赋给变量num。被调函数不知道也不关心传入的数值是来自常量、变量还是一般表达式。再次强调,实际参数是具体的值,该值要被赋给作为形式参数的变量(见图9.3)。因为被调函数使用的值是从主调函数中拷贝而来,所以无论被调函数对拷贝数据进行什么操作,都不会影响主调函数中的原始数据。

Formal parameters and actual arguments

注意:

实际参数和形式参数实际参数是出现在函数调用圆括号中的表达式。形式参数是函数定义的函数头中声明的变量。调用函数时,创建了声明为形式参数的变量并初始化为实际参数的求值结果。程序清单9.2中,\’*\’和WIDTH都是第1次调用show_n_char()时的实际参数,而SPACE和11是第2次调用show_n_char()时的实际参数。在函数定义中,ch和num都是该函数的形式参数。

从黑盒的视角看show_n_char(),待显示的字符和显示的次数是输入。执行后的结果是打印指定数量的字符。输入以参数的形式被传递给函数。这些信息清楚地表明了如何在main()中使用该函数。而且,这也可以作为编写该函数的设计说明。

黑盒方法的核心部分是:ch、num和count都是show_n_char()私有的局部变量。如果在main()中使用同名变量,那么它们相互独立,互不影响。

也就是说,如果main()有一个count变量,那么改变它的值不会改变show_n_char()中的count,反之亦然。黑盒里发生了什么对主调函数是不可见的。

前面介绍了如何把信息从主调函数传递给被调函数。反过来,函数的返回值可以把信息从被调函数传回主调函数。为进一步说明,我们将创建一个返回两个参数中较小值的函数。由于函数被设计用来处理int类型的值,所以被命名为imin()。另外,还要创建一个简单的main(),用于检查imin()是否正常工作。这种被设计用于测试函数的程序有时被称为驱动程序(driver),该驱动程序调用一个函数。如果函数成功通过了测试,就可以安装在一个更重要的程序中使用。程序清单9.3演示了这个驱动程序和返回最小值的函数。

回忆一下,scanf()返回成功读取数据的个数,所以如果输入不是两个整数会导致循环终止。下面是一个运行示例:

关键字return后面的表达式的值就是函数的返回值。在该例中,该函数返回的值就是变量min的值。因为min是int类型的变量,所以imin()函数的类型也是int。

变量min属于imin()函数私有,但是return语句把min的值传回了主调函数。下面这条语句的作用是把min的值赋给lesser:

是否写成下面这样:

不能。因为主调函数甚至不知道min的存在。记住,imin()中的变量是imin()的局部变量。函数调用imin(evil1, evil2)只是把两个变量的值拷贝了一份。

返回值不仅可以赋给变量,也可以被用作表达式的一部分。例如,可以这样:

返回值不一定是变量的值,也可以是任意表达式的值。例如,可以用以下的代码简化程序示例:

条件表达式的值是n和m中的较小者,该值要被返回给主调函数。虽然这里不要求用圆括号把返回值括起来,但是如果想让程序条理更清楚或统一风格,可以把返回值放在圆括号内。

如果函数返回值的类型与函数声明的类型不匹配会怎样?

实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值。因此在本例中,相当于把z的值赋给int类型的变量,然后返回int类型变量的值。例如,假设有下面的函数调用:

虽然在what_if()函数中赋给z的值是1.5625,但是return语句返回int类型的值1。

使用return语句的另一个作用是,终止函数并把控制返回给主调函数的下一条语句。因此,可以这样编写imin():

许多C程序员都认为只在函数末尾使用一次return语句比较好,因为这样做更方便浏览程序的人理解函数的控制流。但是,在函数中使用多个return语句也没有错。无论如何,对用户而言,这3个版本的函数用起来都一样,因为所有的输入和输出都完全相同,不同的是函数内部的实现细节。下面的版本也没问题:

return语句导致printf()语句永远不会被执行。如果Fleppard教授在自己的程序中使用这个版本的函数,可能永远不知道编写这个函数的学生对他的看法。另外,还可以这样使用return:

这条语句会导致终止函数,并把控制返回给主调函数。因为return后面没有任何表达式,所以没有返回值,只有在void函数中才会用到这种形式。

声明函数时必须声明函数的类型。带返回值的函数类型应该与其返回值类型相同,而没有返回值的函数应声明为void类型。如果没有声明函数的类型,旧版本的C编译器会假定函数的类型是int。这一惯例源于C的早期,那时的函数绝大多数都是int类型。然而,C99标准不再支持int类型函数的这种假定设置。

类型声明是函数定义的一部分。要记住,函数类型指的是返回值的类型,不是函数参数的类型。例如,下面的函数头定义了一个带两个int类型参数的函数,但是其返回值是double类型。

要正确地使用函数,程序在第1次使用函数之前必须知道函数的类型。方法之一是,把完整的函数定义放在第1次调用函数的前面。然而,这种方法增加了程序的阅读难度。而且,要使用的函数可能在C库或其他文件中。因此,通常的做法是提前声明函数,把函数的信息告知编译器。

第2行代码说明imin是一个函数名,有两个int类型的形参,且返回int类型的值。现在,编译器在程序中调用imin()函数时就知道应该如何处理。

我们把函数的前置声明放在主调函数外面。当然,也可以放在主调函数里面。例如,重写lesser.c 的开头部分:

注意在这两种情况中,函数原型都声明在使用函数之前。

ANSI-C标准库中,函数被分成多个系列,每一系列都有各自的头文件。这些头文件中除了其他内容,还包含了本系列所有函数的声明。例如,stdio.h头文件包含了标准I/O库函数(如,printf()和scanf())的声明。math.h头文件包含了各种数学函数的声明。例如,下面的声明:

告知编译器sqrt()函数有一个double类型的形参,而且返回double类型的值。不要混淆函数的声明和定义。函数声明告知编译器函数的类型,而函数定义则提供实际的代码。在程序中包含math.h头文件告知编译器:sqrt()返回double类型,但是sqrt()函数的代码在另一个库函数的文件中。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。