第10章写程序就是写函数——函数入门•相信读者大致都了解一点数学意义上“函数”的概念,比如“y=f(x)”,且不论f的具体形式如何,其基本特点是“对一个x,有一个y值与之对应”。C语言中,“函数”是个重要的概念,是模块化编程的基础。•本章主要涉及函数的概念、函数原型、函数的定义、函数的参数传递机制等相对基础的内容,为后面进一步阐述模块化编程打下基础。10.1什么是函数?—根据输入进行处理返回输出•代码编多了会发现一个问题:一些通用的操作,比如交换两个变量的值,对一组变量进行排序等,可能在多个程序中都会用到,不仅如此,在单独一个程序中也可能会对某个代码段执行多次。•问题:有必要在每次执行时都把该代码段书写一次么,这不仅会让程序变得很长,且会造成难以理解,可读性下降。10.1.1分割•为了解决以上问题,C语言将程序按功能分割成一系列的小模块,所谓“小模块”,可理解为完成一定功能的可执行代码块,称之为“函数”。•函数是C语言源程序的基本功能单位,打个比方,可以将函数视为一个黑盒子,或“加工设备”,从一头输入数据(原材料),从另一头就可以得到结果(产品)。至于函数内部是如何工作的,外部并不关心。•C语言源程序均是由函数组成的,在前面章节给出的示例代码,只有一个main函数,这仅适用于比较简单的问题,实际上的程序往往由多个程序组成。函数的调用是由另一个函数发起的,举例来说,在A函数中调用B函数,从B函数的角度上说,A函数可视为外部函数(有的书中也叫外部程序、主调函数,B函数相应地称为被调函数),外部函数A对函数B是如何定义的,功能是如何实现的毫不关心,A对B所知道的仅限于输入给B什么,以及B会输出什么。10.1.2库函数和自定义函数•为方便解决某些基本问题,C语言提供了库函数,库函数是将前人书写的、有通用功能的函数打包,方便开发者调用,或是一些复杂的功能,比如输入输出,这涉及到硬件方面的内容,如果要我们自己写如何输出输出的函数,肯定要头大半天。•C语言中的库函数十分丰富,大致可分为“标准库函数”和“第三方库函数”两类,标准库函数是得到广泛认可,形式统一,被多种类的编译器支持的库函数,而第三方库函数是一些软件厂商为某些特定功能领域开发,多具有专用性。随着C语言的不断发展和应用领域的拓展,标准化的工作也在不断深入,标准库函数也会不断扩充。•除了库函数外,C语言允许用于自定义函数以灵活解决各种问题,用户可以将自己的算法编成一个个相对独立的函数模块,用调用的方法来使用函数。某种程度上说,C语言的全部功能是由这样那样的函数来实现的,C语言也常称为“函数式语言”。10.2自定义函数•函数的调用可能是由另一个函数触发,但函数的定义都是平行的,包括main函数在内,所谓“平行”,有两层含义,一是“不允许把一个函数定义在另一个函数内”,这说明,函数定义都要在main函数外部,二是“不同函数定义放置位置没有关系”,可以定义在main函数前,也可以定义在main函数之后。10.2.1定义的语法•和变量一样,要想使用一个函数,定义是不可缺少的,函数定义有4个要素:参数列表,返回类型,函数名和函数体,参数列表和返回类型对应着输入输出,函数名用于和程序中其他程序实体区分,而函数体是一段可执行的代码块,实现特定的算法或功能。•函数的基本定义语法如下:•返回类型函数名(参数列表)•{•函数体;•}•(1)输入:参数列表•基本形式为:•类型变量名1,类型变量名2,类型变量名3,……•(2)输出:返回类型•返回类型用于指明函数输出值的类型,如果没有输出值,返回类型为void。如果在函数定义时没有注明返回类型,默认为int。•(3)函数名•函数名用于标示该函数,和其他函数区分开来,因此,函数名必须是合乎编译器命名规则的标识符。•参数列表、返回类型和函数名总体称为函数头,与之对应的是函数体。•(4)函数体•函数体是一段用于实现特定功能的代码块,比如局部变量声明和其他执行语句等。注意,在函数体内声明的变量不能和参数列表中的变量同名。10.2.2函数定义范例•定义一个函数是为了调用,函数调用有两种类型,一是“先定义,后调用”,这要求函数定义和调用语句在同一个文件内,编译器能从函数定义中提取函数的参数列表、输出类型等接口信息。二是“函数声明+函数调用”,大多数情况下,函数的定义与函数的调用并不在一个文件内,即使在一个文件中也有可能调用在前而定义在后,这时需要在调用之前先对函数声明,告诉编译器有这么一个函数存在,函数原型声明将在下一节讨论,下面来看一个先定义、后调用的例子,希望读者从中体会函数的定义和调用方式,见示例。10.2.3不要重复定义•一个C程序可能由多个文件(一个.c、.h等称为一个文件)组成,函数的定义和函数的调用可能位于不同的文件中,此时需要在调用前对函数进行原型声明,这在稍后章节中会提及。但有一条准则,对一个函数来说,在整个程序中只能有一个定义版本,否则,编译器会指出“重复定义”的错误。10.3函数调用与返回•定义一个函数的目的是为了使用它,中在main函数中调用了bigger函数,可能有的读者会问“为什么要写成“intres=bigger(num1,num2);”的形式?本节将重点讨论函数调用与值返回相关的内容。10.3.1形参和实参•在函数定义时参数列表中是a和b,而在函数调用时传递进来的参数是num1和num2,这两种参数是什么关系呢?打个形象的比方,这是角色和演员的关系。•函数定义时列表中的参数称为形参,是“剧本角色”,而函数调用时传递进来的参数称为实参,是“演员”,函数执行的过程就是演戏的过程。•程序刚开始执行的时候,编译器并不为形参分配存储空间,因为它只是个角色,不是实体,一直要到函数调用时,编译器为形参分配存储空间,并将实参的值复制给形参,结合。可知,在“intres=bigger(num1,num2);”语句调用前,a和b都不是真正的程序变量,一直到bigger函数被调用,a和b才被创建,并分别用num1和num2为其赋值,找这种情况下,在函数内对a和b的处理并不影响num1和num2,这类似于“某个演员扮演的角色在戏中受伤,并不是说演员真的受伤了”,而且,在函数执行结束返回时,创建的形参被撤销,这类似于“戏演完了,剧中角色自然也就停止了”。10.3.2传址调用•在介绍传址调用前,读者应了解下指针的概念,这是C语言的难点所在,后续章节中会详细介绍指针的知识,本节只是简要介绍其意义,说白了,指针也是一种变量类型,与普通变量不同之处在于:指针类似于生活中的地址,通过指针可以访问其指向的变量,正如通过地址能找到某个人一样。•地址类型是种复合类型,大致形式为“类型*”,如:•int*pInt=NULL;•doublenum1=8;•double*pDouble=&num1;•注:NULL代表将该指针初始化为空。•不管类型如何,指针变量占据4个字节,但其指向的对象占据的字节数不一定为4(如double类型的指针指向一片大小为8的内存),指针变量的值是其指向的内存首字节的地址,“&num1”中符号“&”是取地址符,返回的是变量num1的地址,指针及其指向内存的简单示意如所示:10.3.3函数返回•请读者返回看一下前面的,里面bigger函数定义如下:•intbigger(inta,intb)/*函数头,返回类型函数名(参数列表)*/•{•if(ab)•returna;/*返回值*/•else•returnb;•}•既然说a和b都是实参,在程序被调用时方才创建,程序退出时便被撤销,那诸如“returna;”之类的返回语句岂不是没有意义,返回的是一个被撤销的量?函数的返回机制应如何理解呢。•理解的关键词是“复制”,执行到return语句时,return的值被复制到某个内存单元或寄存器中,其地址是由编译器来维护的,我们不用操心,也就是说,在a和b被撤销前,返回的值(a或b)被复制保存到了某个地方,编译器访问该内存单元即可知道函数的返回值,下述语句:•intres=bigger(num1,num2);/*函数调用,值的返回*/10.4告诉编译器有这么一个函数——函数原型声明•上面章节中定义的函数都是在main函数之前,这样,编译器在调用该函数时已经知道了该函数的存在,明确了其接口,但有时,函数定义和函数调用并不在一个文件中,或即使在一个文件中,但定义顺序不好安排,还有就是那些不须定义的库函数等,如果要想正确处理这些情况,必须理解函数声明的概念。10.4.1函数声明语法•第3章中有一节内容(“编译器如何认识printf函数”)已经简要介绍了函数原型声明的内容,是否还记得这段代码:•最上面两条语句即是函数原型声明语句,调用函数之前,一定要使得编译器知道函数原型,这样编译器才知道有哪些函数名,该函数需要些什么样类型的参数,返回什么样类型的数值。•函数原型声明的基本形式如下:•返回类型函数名(数据类型形参,数据类型形参,数据类型形参,……);•或•返回类型函数名(数据类型,数据类型,数据类型……)10.4.2声明不同于定义•函数的声明和定义不同,总的来说有以下几点:•(1)函数定义4个要素不可或缺,是一个完整的函数实现形式,包括返回类型、形参、函数名和最重要的函数体的定义,而函数声明被用来通知编译器被调函数的返回类型、名称和参数类型信息,相当于“接口”,声明时没有函数体而且形参的类型是关心的要点,而形参的名称在声明时可省略。•(2)在某些情况下,函数的声明可以省略,如函数先定义,后调用的情况,但函数的定义不能省略,且只能定义一次,而且,定义要比声明更为复杂和完整。•(3)函数定义结束时不用加分号,而声明结束时必须加分号,因此,直接在函数头后添上分号作为函数的声明是个不错的方法。•特别强调:函数的原型声明应与函数头保持一致,否则,编译器会报错。10.4.3标准库函数的声明•前面提及,为了方便用户实现各种复杂的操作,C标准中定义了很多标准的库函数,这些库函数是以二进制代码库的形式提供的,也就是说,在安装了编译器及相应库文件后,用户编程中接触到的库函数并不是以源文件.c形式提供的,而是在编译阶段链接进来的,而头文件则是对应库函数原型的列表,只要包含了头文件,就对该头文件对应的所有库函数进行了声明。10.5面向过程的程序结构•在60年代计算机发展的初期,程序设计是少数聪明人的玩具,程序员可以根据自己的喜好,像捏泥巴一样进行程序设计,注释几乎是一行没有,想到哪写到哪,大多数程序代码组织混乱,可以说只有作者本人可以看懂,有的甚至作者读起来也不知所以,常称为被称为“意大利面条式编程”。•这种个人英雄主义的单打独斗在解决小规模问题时勉强可以,但程序规模的不断扩大,一大堆的问题凸现出来:程序质量低下,进度延误,预算严重超支,这就是“软件危机”,给程序开发的前景蒙上了一层暗淡的色彩。•结构化程序设计方法就是在这个背景下提出的,除了前面章节讲过的3种控制结构:顺序、分支和循环外,结构化程序设计的另一个关键概念是模块化设计。10.5.1模块化•生活中常常接触到模块化的概念,模块化程序设计大致有点像小时候玩的积木游戏,用木块组合的方式很容易地就构筑起了“大厦”,模块化至少有两点好处:一是封装,“积木块”是“基本砖块”的组合,对外是个整体,使用方便,二是可复用,“柱子”封装好后,既可以用在这个建筑上,又可以用在那个建筑上。程序设计也可以借鉴这一思想,用模块化的方法进行程序设计,函数正是模块化方法的体现。•虽说语句是C语言的基本单位,但从程序设计总体把握上来看,将函数视为一个整体,大大降低了问题的复杂程度。在解决复杂问题时,首先考虑的是问题的概貌,而不是微小细节,这是人的思维和行动习惯,程序设计也是如此,先将问题分割成一个个函数,每个函数实现特定的功能,确定函数之间的联系和依赖关系,这是从整体解决某个问题。其次才是考虑每个函数应怎么写,算法流程怎么走这些问题,这就是“分而治之、逐步求精“的设计方法学。10.5.2函数的调用过程—模块的配合•C语言是由函数组成的,第10章中已经介绍了函数的定