2022.1.19 C语言设计(第四版)谭浩强 Day3 第七章 用函数实现模块化程序设计

一、概述

1、一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。对较大的程序,一般不希望把所有内容全放在一个文件中,而是将它们分别放在若干个源文件中,由若干个源程序文件组成一个C程序。这样便于分别编写和编译,提高调试效率。一个源程序文件可以为多个C程序共用。

2、一个源程序文件由一个或多个函数以及其他有关内容(如指令、数据声明与定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。

3、C程序的执行是从main函数开始的,如果在main函数中调用其他函数,在调用后流程返回到main函数,在main函数中结束整个程序的运行。

4、所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数间可以互相调用,但不能调用main函数。main函数是被操作系统调用的。

5、从用户使用的角度看,函数有两种。
      (1)库函数,它是由系统提供的,用户不必自己定义,可直接使用它们。应该说明,不同的C语言编译系统提供的库函数的数量和功能会有一些不同,当然许多基本的函数是共同的。
       (2)用户自己定义的函数。它是用以解决用户专门需要的函数。

6、从函数的形式看,函数分两类。
      (1)无参函数。在调用无参函数时,主调函数不向被调用函数传递数据。无参函数一般用来执行指定的一组操作。无参函数可以带回或不带回函数值,但一般以不带回函数值的居多。
       (2)有参函数。在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据,一般情况下,执行被调用函数时会得到一个函数值,供主调函数使用。此时有参函数应定义为与返回值相同的类型。

二、定义函数

1、包含内容

(1)指定函数的名字,以便以后按名调用。

(2)指定函数的类型,即函数返回值的类型。

(3〉指定函数的参数的名字和类型,以便在调用函数时向它们传递数据。对无参函数不需要这项

(4)指定函数应当完成什么操作,也就是函数是做什么的,即函数的功能。这是最重要的,是在函数体中解决的。

2、库函数

        对于C编译系统提供的库函数,是由编译系统事先定义好的,库文件中包括了对各函数的定义。程序设计者不必自己定义,只须用#include指令把有关的头文件包含到本文件模块中即可。在有关的头文件中包括了对函数的声明。

        例如,在程序中若用到数学函数(如sqrt, fabs,sin,cos等),就必须在本文件模块的开头写上:
#include math.h>。
        库函数只提供了最基本,最通用的一些函数,而不可能包括人们在实际应用中所用到的所有函数。程序设计者需要在程序中自己定义想用的而库函数并没有提供的函数。


3、定义方法

(1)定义无参函数

类型名        函数名()                                  类型名        函数名(void)

{                                                                      {

           函数体                                   或                     函数体

}                                                                      {

①函数名后面的括号内的void表示“空”,即函数没有参数

②函数体包括声明部分和语句部分

③在定义函数时要用“类型标识符”(即类型名)指定函数值的类型,即指定函数带回来的值的类型

(2)定义有参函数

类型名        函数名(形式参数表列)

{

             函数体

}

函数体包括声明部分和语句部分

(3)定义空函数

类型名        函数名()

{}

①函数体是空的。调用此函数时,什么工作也不做,没有任何实际作用。

②在程序设计中往往根据需要确定若干个模块,分别由一些函数来实现。而在第1阶段只设计最基本的模块,其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数(函数名取将来采用的实际函数名(如用merge() ,matproduct() ,concatenate()和 shell()等,分别代表合并、矩阵相乘、字符串连接和希尔法排序等),只是这些函数暂时还未编写好,先用空函数占一个位置,等以后扩充程序功能时用一个编好的函数代替它。这样做,程序的结构清楚,可读性好,以后扩充新功能方便,对程序结构影响不大。空函数在程序设计中常常是有用的。

三、调用函数

1、一般形式

函数名(实参表列)

如果调用无参函数,则“实参表列”可以没有,但括号不能省略。如果实参表列包含多个实参,则各参数间用逗号隔开。

2、调用方式

(1)函数调用语句

          把函数调用单独作为一个语句。

          如:    “printf_star();”

          这时不要求函数带回值,只要求函数完成一定的操作。

(2)函数表达式

          函数调用出现在另一个表达式中。

          如:     “c=max(a,b);”,

           max(a, b)是一次函数调用,它是赋值表达式中的一部分。这时要求函数带回一个确定的值以参加表达式的运算。例如:

                        c=2 * max(a,b);

(3)函数参数

          函数调用作为另一个函数调用时的实参。

          如:          m=max(a, max( b,c));
          其中max(b,c)是一次函数调用v它的值作为max另一次调用的实参。经过赋值后,m的值是a,b,c三者中的最大者。

          又如:        printf("%d",max ( a,b)>﹔
          也是把max(a,b)作为printf函数的个参数。

3、说明

         调用函数并不一定要求包括分号(如print_star( );),只有作为函数调用语句才需要有分号。如果作为函数表达式或函数参数,函数调用本身是不必有分号的。

         不能写成
                           printf ("%d',max (a,b););                                         // max (a,b)后面多了一个分号

4、(有参)函数调用时的数据传递——主调函数与被调用函数

(1)形式参数

         在定义函数时函数名后面括号中的变量名称为“形式参数”(简称“形参")或“虚拟参数”。

(2)实际参数

         在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”(简称“实参")。

         实际参数可以是常量、变量或表达式。

(3)实参和形参间的数据传递

         在调用函数过程中,系统会把实参的值传递给被调用函数的形参。或者说,形参从实参得到一个值。该值在函数调用期间有效,可以参加该函数中的运算。
         在调用函数过程中发生的实参与形参间的数据传递,常称为“虚实结合”。

5、函数调用的过程

(1)在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。在发生函数调用时,函数max 的形参被临时分配内存单元。

(2)将实参对应的值传递给形参。如图7.3所示,实参的值为2,把⒉传递给相应的形参x,这时形参x就得到值2,同理,形参y得到值3。

(3)在执行max函数期间,由于形参已经有值,就可以利用形参进行有关的运算(例如把x和y比较,把x或y的值赋给z等)。

(4)通过return语句将函数值带回到主调函数。例7.2中在return语句中指定的返回值是z,这个z就是函数max的值(又称返回值)。执行return语句就把这个函数返回值带回主调函数main。应当注意返回值的类型与函数类型一致。如max函数为int型,返回值是变量z,也是int型。二者一致。
如果函数不需要返回值,则不需要return语句。这时函数的类型应定义为void类型。

(5)调用结束,形参单元被释放。注意:实参单元仍保留并维持原值,没有改变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参的值。

例如,若在执行max函数过程中x和y的值变为10和15,但a和 b仍为2和3。

这是因为实参与形参是两个不同的存储单元。
 

6、函数的返回值

(1)函数值/函数的返回值:通过函数调用使主调函数能得到一个确定的值

(2)函数的返回值是通过函数中的return语句获得的。“return z;”  等价于  “ return(z);”

(3)函数值的类型:在定义函数时指定函数值的类型

(4)在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致。若不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换,即函数类型决定返回值类型。

(5)对于不带返回值的函数,应当定义函数为“void类型”。这样,系统就保证不知函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。此时在函数体中不得出现return语句。

四、对被调用的函数的声明和函数原型

(1)在一个函数中调用另一个函数需要具备的条件

①被调用的函数必须是已经定义的函数(库函数/用户自己定义的函数)

②若使用库函数,应该在本文件开头用#include指令将调用有关库函数时所需用到的信息“包含“到本文件中来。

③如果使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数)的后面(在同一个文件中),应该在主调函数中对被调用的函数作声明(declaration)。声明的作用是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法。

(2)函数声明

          函数的声明和函数定义中的第1行(函数首部)基本上是相同的,只差一个分号(函数声明比函数定义中的首行多一个分号)。因此写函数声明时,可以简单地照写已定义的函数的首行,再加一个分号,就成了函数的“声明”。

          函数的首行(即函数首部)称为函数原型(function prototype)。为什么要用函数的首部来作为函数声明呢?这是为了便于对函数调用的合法性进行检查。因为在函数的首部包含了检查调用函数是否合法的基本信息(它包括了函数名﹑函数值类型、参数个数、参数类型和参数顺序),在检查函数调用时要求函数名、函数类型、参数个数和参数顺序必须与函数声明一致,实参类型必须与函数声明中的形参类型相同(或赋值兼容,如实型数据可以传递给整型形参,按赋值规则进行类型转换)。否则就按出错处理。这样就能保证函数的正确调用。

          使用函数原型作声明是C的一个重要特点。用函数原型来声明函数,能减少编写程序时可能出现的错误。由于函数声明的位置与函数调用语句的位置比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。

          实际上,在函数声明中的形参名可以省写,而只写形参的类型

          如上面的声明可以写为          float add(float,float);
          编译系统只关心和检查参数个数和参数类型,而不检查参数名,因为在调用函数时只要求保证实参类型与形参类型一致,而不必考虑形参名是什么。因此在函数声明中,形参名可写可不写、形参名是什么都无所谓,

          如:          float add( float a,float b);                    //参数名不用x,y,而用a,b。合法

(3)函数原型

            一般形式

函数类型    函数名(参数类型1    参数名1,参数类型2    参数名2,···,参数类型n    参数名n);

函数类型    函数名(参数类型1,参数类型2,···,参数类型n);

五、函数的嵌套调用

两层嵌套

六、函数的递归调用

          在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。

          C语言的特点之一就在于允许函数的递归调用。

七、数组作为函数参数

1、数组元素作函数实参

        数组元素可以用作函数实参,不能用作形参。因为形参是在函数被调用时临时分配存储单元的,不可能为一个数组元素单独分配存储单元(数组是一个整体,在内存中占连续的一段存储单元)。在用数组元素作函数实参时,把实参的值传给形参,是“值传递”方式。数据传递的方向是从实参传到形参,单向传递。

2、数组名作函数参数

        除了可以用数组元素作为函数参数外,还可以用数组名作函数参数(包括实参和形参)。

        应当注意的是:用数组元素作实参时,向形参变量传递的是数组元素的值,而用数组名作函数实参时,向形参(数组名或指针变量〉传递的是数组首元素的地yi

(1)用数组名作函数参数,应该在主调函数和被调用函数分别定义数组,

  (2)实参数组与形参数组类型应一致(今都为float型),如不一致,结果将出错。

  (3)在定义average函数时,声明数组的大小为10,但在实际上,指定其大小是不起任何作用的,因为C语言编译系统并不检查形参数组大小,只是将实参数组的首元素的地址传给形参数组名。因此,形参数组名获得了实参数组的首元素的地址,前已说明,数组名代表数组的首元素的地址,因此,形参数组首元素(array[o])和实参数组首元素(score[0])具有同一地址,它们共占同一存储单元,score[n]和 array[n]指的是同一单元。score[n]和array[n]具有相同的值。

  (4)形参数组可以不指定大小,在定义数组时在数组名后面跟一个空的方括号,效果是相同的。在学习了第8章后,可以知道在编译时把形参数组名处理为一个指针变量,用来接收一个地址。
 

(3)多维数组名作函数参数

        可以用多维数组名作为函数的实参和形参,在被调用函数中对形参数组定义时可以指定每一维的大小,也可以省略第一维的大小说明。例如:
        int array[3][10]或int array[][10]都是合法且等价的。

八、局部变量和全局变量(从变量的作用域分类)

(1)变量

        (1)在函数的开头定义;

        (2)在函数内的复合语句内定义;

        (3)在函数的外部定义。

(2)局部变量

        在一个函数内部定义的变量只在本函数范围内有效,也就是说只有在本函数内才能引用它们,在此函数以外是不能使用这些变量的。

        在复合语句内定义的变量只在本复合语句范围内有效,只有在本复合语句内才能引用它们。在该复合语句以外是不能使用这些变量的,以上这些称为“局部变量”。

(3)全局变量

        程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量称为外部变量,外部变量是全局变量(也称全程变量)。全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。

🐖:为了便于区别全局变量和局部变量,在C程序设计人员中有一个习惯(但非规定),将全局变量名的第1个字母用大写表示。
 

九、变量的存储方式和生存期(从变量存在的时间分类)

(1)静态储存与动态存储

        有的变量在程序运行的整个过程都是存在的,而有的变量则是在调用其所在的函数时才临时分配存储单元,而在函数调用结束后该存储单元就马上释放了,变量不存在了。

        也就是说,变量的存储有两种不同的方式:静态存储方式和动态存储方式。

        静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式,而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。

        内存中的供用户使用的存储空间可以分为3部分:

  • (1〉程序区;
  • (2)静态存储区﹔                                                                                            
  • (3)动态存储区。

 

        数据分别存放在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。

        在动态存储区中存放以下数据:

  • 函数形式参数。在调用函数时给形参分配存储空间。
  • 函数中定义的没有用关键字static声明的变量,即自动变量(详见后面的介绍)。
  • 函数调用时的现场保护和返回地址等。

        对以上这些数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的,如果在一个程序中两次调用同一函数,而在此函数中定义了局部变量,在两次调用时分配给这些局部变量的存储空间的地址可能是不相同的。

        在C语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别。对数据类型,读者已经熟知(如整型、浮点型等)。存储类别指的是数据在内存中存储的方式(如静态存储和动态存储)。
在定义和声明变量和函数时,一般应同时指定其数据类型和存储类别,也可以采用默认方式指定(即长如果用户不指定,系统会隐含地指定为某一种存储类别)。
        C的存储类别包括4种:自动的( auto),静态的( statis)、寄存器的( register)、外部的( extern)。根据变量的存储类别,可以知道变量的作用域和生存期。

(2)局部变量的存储类别

①自动变量(auto变量)

        函数中的局部变量,如果不专门声明为static(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量),都属于此类。在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字auto 作存储类别的声明。

②静态局部变量(static局部变量)

        有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用该函数时,该变量已有值(就是上一次函数调用结束时的值)。这时就应该指定该局部变量为“静态局部变量”,用关键字static进行声明

        静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,分配在动态存储区空间而不在静态存储区空间,函数调用结束后即释放。

        对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行的,每调用一次函数重新给一次初值,相当于执行一次赋值语句。

        需要保留函数上一次调用结束时的值时,需要用局部静态变量。
 

③寄存器变量(register变量)

        如果有一些变量使用频繁,为提高执行效率,允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做寄存器变量,用关键字register作声明。   

(3)全局变量的存储类别

        全局变量都是存放在静态存储区中的。因此它们的生存期是固定的,存在于程序的整个运行过程。但是,对全局变量来说,还有一个问题尚待解决,就是它的作用域究竟从什么位置起,到什么位置止。作用域是包括整个文件范围,还是文件中的一部分范围?是在一个文件中有效,还是在程序的所有文件中都有效?这就需要指定不同的存储类别。
一般来说,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以为程序中各个函数所引用。但有时程序设计人员希望能扩展外部变量的作用域。有以下几种情况:

①在一个文件内扩展外部变量的作用域(extern)

        如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果由于某种考虑,在定义点之前的函数需要引用该外部变量,则应该在引用之前用关键字extern对该变量作“外部变量声明”,表示把该外部变量的作用域扩展到此位置。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

②将外部变量的作用域扩展到其他文件(extern)

        如果---个程序包含两个文件,在两个文件中都要用到同一个外部变量Num,不能分别在两个文件中各自定义一个外部变量Num,否则在进行程序的连接时会出现“重复定义”的错误。

        正确的做法是:在任一个文件中定义外部变量Num,而在另一文件中用extern对Num作“外部变量声明”,即“extern Num; ”。在编译和连接时,系统会由此知道Num 有“外部链接”,可以从别处找到已定义的外部变量Num,并将在另一文件中定义的外部变量num的作用域扩展到本文件,在本文件中可以合法地引用外部变量Num。

         extern 既可以用来扩展外部变量在本文件中的作用域,又可以使外部变量的作用域从一个文件扩展到程序中的其他文件,那么系统怎么区别处理呢?

        实际上,在编译时遇到extern时,本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域;如果找不到,就在连接时从其他文件中找外部变量的定义。如果从其他文件中找到了,就将作用域扩展到本文件;如果再找不到,就按出错处理。 

③将外部变量的作用域限制在本文件中(static)

        有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明

用static声明一个变量的作用是:
(1)对局部变量用static声明,把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在。
(2)对全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。

(4)存储类别小结

 

 

十、关于变量的声明和定义

(1)声明

        声明部分出现的变量有两种情况:一种是需要建立存储空间的(如“int a;”),另一种是不需要建立存储空间的(如“externa;”)。前者称为定义性声明(defining declaration),或简称定义(definition);后者称为引用性声明(referencing declaration)。广义地说,声明包括定义,但并非所有的声明都是定义。对“int a;”而言,它既是声明,又是定义;而对“extern a;”而言,它是声明而不是定义。一般为了叙述方便,把建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明。显然这里指的声明是狭义的,即非定义性声明。

(2)定义

        外部变量定义和外部变量声明的含义是不同的。外部变量的定义只能有一次,它的住置在所有函数之外。在同一文件中,可以有多次对同-外部变量的声明,它的位置可以在函数之内(哪个函数要用就在哪个函数中声明),也可以在函数之外(在外部变量的定义点之前)。系统根据外部变量的定义(而不是根据外部变量的声明)分配存储单元。对外部变量的初始化只能在“定义”时进行,而不能在“声明”中进行。

        所谓“声明”,其作用是声明该变量是一个已在其他地方已定义的外部变量,仅仅是为了扩展该变量的作用范围而作的“声明”。

        有一个简单的结论,在函数中出现的对变量的声明(除了用extern声明的以外)都是定义。在函数中对其他函数的声明不是函数的定义。
 

十一、内部函数和外部函数

        函数本质上是全局的,因为定义一个函数目的就是要被另外的函数调用。如果不加声明的话,一个文件中的函数既可以被本文件中其他函数调用,也可以被其他文件中的函数调用。但是,也可以指定某些函数不能被其他文件调用。根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。

(1)内部函数

        如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加static,即:

static        类型名        函数名(形参表);

例如,函数的首行:
static           int f           un(int a,int b)

表示fun是一个内部函数,不能被其他文件调用。

        内部函数又称静态函数,因为它是用static声明的。使用内部函数,可以使函数的作用域只局限于所在文件。这样,在不同的文件中即使有同名的内部函数,也互不干扰。这就使它对外界“屏蔽”了。通常,一个大程序往往分工由不同的人分别编写不同的文件模块,在各人编写自己的文件模块时,不必担心所用函数是否会与其他文件模块中的函数同名。
        通常把只能由本文件使用的函数和外部变量放在文件的开头,前面都冠以static使之局部化..其他文件不能引用,这就提高了程序的可靠性.

(2)外部函数

        如果在定义函数时,在函数首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用。

如函数首部可以为
extern int fun ( int a,int b)

这样,函数fun就可以为其他文件调用。C语言规定,如果在定义函数时省略extern,则默认为外部函数。本书前面所用的函数都是外部函数。

        在需要调用此函数的其他文件中,需要对此函数作声明(不要忘记,即使在本文件中调用一个函数,也要用函数原型进行声明)。在对此函数作声明时,要加关键字extern,表示该函数“是在其他文件中定义的外部函数”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值