第3章 函数和编译预处理(1)lgl_第1页
第3章 函数和编译预处理(1)lgl_第2页
第3章 函数和编译预处理(1)lgl_第3页
第3章 函数和编译预处理(1)lgl_第4页
第3章 函数和编译预处理(1)lgl_第5页
已阅读5页,还剩126页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第3章函数和编译预处理3.1函数概述

3.2函数的定义和调用

3.3函数的参数传递

3.4函数的嵌套调用和递归调用

3.5内置函数

3.6变量和函数的属性

3.7编译预处理

3.1概述一个较大的程序不可能完全由一个人从头至尾地完成,更不可能把所有的内容都放在一个主函数中。为了便于规划、组织、编程和调试,一般的做法是把一个大的程序划分为若干个程序模块(即程序文件),每一个模块实现一部分功能。不同的程序模块可以由不同的人来完成。在程序进行编译时,以程序模块为编译单位,即分别对每一个编译单位进行编译。如果发现错误,可以在本程序模块范围内查错并改正。在分别通过编译后,才进行连接,把各模块的目标文件以及系统文件连接在一起形成可执行文件。在一个程序文件中可以包含若干个函数。无论把一个程序划分为多少个程序模块,只能有一个main函数。程序总是从main函数开始执行的。在程序运行过程中,由主函数调用其他函数,其他函数也可以互相调用。在C语言中没有类和对象,在程序模块中直接定义函数。可以认为,一个C程序是由若干个函数组成的,C语言被认为是面向函数的语言。C++面向过程的程序设计沿用了C语言使用函数的方法。在C++面向对象的程序设计中,主函数以外的函数大多是被封装在类中的。主函数或其他函数可以通过类对象调用类中的函数。无论是C还是C++,程序中的各项操作基本上都是由函数来实现的,程序编写者要根据需要编写一个个函数,每个函数用来实现某一功能。因此,读者必须掌握函数的概念以及学会设计和使用函数。“函数”这个名词是从英文function翻译过来的,其实function的原意是“功能”。顾名思义,一个函数就是一个功能。在实际应用的程序中,主函数写得很简单,它的作用就是调用各个函数,程序各部分的功能全部都是由各函数实现的。主函数相当于总调度,调动各函数依次实现各项功能。开发商和软件开发人员将一些常用的功能模块编写成函数,放在函数库中供公共选用。程序开发人员要善于利用库函数,以减少重复编写程序段的工作量。图3.1是一个程序中函数调用的示意图。图3.1main()func1()func2()func3()func5()func4()例3.1在主函数中调用其他函数。#include<iostream.h>//*****ex3_1.cpp*****intcalc_sum(intn)//定义calc_sum()函数{intk,s;s=0;for(k=1;k<=n;k++)s=s+k;

returns;}voidprint_word(void)//定义print_word()函数{cout<<"Hello,C++!"<<endl;//输出一行文字}voidmain(void){intn;cin>>n;if(n>1)cout<<"thesumis:"<<calc_sum(n)<<endl;//调用calc_sum()函数

print_word();//调用rint_word()函数}若用户从键盘输入的数是4,运行情况如下:thesumis:10Hello,C++!

从用户使用的角度看,函数有两种:(1)系统函数,即库函数。这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们。(2)用户自己定义的函数。用以解决用户的专门需要。从函数的形式看,函数分两类:(1)无参函数。调用函数时不必给出参数。(2)有参函数。在调用函数时,要给出参数。在主调函数和被调用函数之间有数据传递。3.2函数的定义和调用3.2.1定义函数的一般形式定义函数的一般形式如下:类型标识符函数名([形式参数列表]){声明语句执行语句}【例3.1】的print_word函数是无参函数,它的定义中首句也可以写成:voidprint_word()//定义print_word()函数的首部;或:voidprint_word(void)//定义print_word()函数的首部【例3.1】的calc_sum函数是有参函数,不过它只有一个参数,参数的类型是整数类型。intcalc_sum(intn)//定义calc_sum()函数

C++要求在定义函数时必须指定函数的类型。下面的函数f则有两个参数,第一个是整数类型,第二个是单精度实数类型,而函数的返回值是双精度实数类型:doublef(intx,floaty)//定义f()函数的首部(1)对库函数的声明其实,对库函数的声明语句已经写在有关包含文件中了,因此只要在程序文件头用include语句将这些包含文件包含到本程序中来,就完成了对库函数的声明。(2)对自定义函数的声明必须在调用某自定义函数的语句之前写上如下声明语句:函数类型关键字函数名([参数1类型,参数1名称][,参数2类型,参数2名称][…]);

3.2.2函数的声明(2)对自定义函数的声明(续)也可以在声明语句中略去参数的名称,或写一个任意名称,这叫做函数原型(functionprototype),即声明函数原型。函数原型有下列两种表示形式:函数类型关键字函数名([参数1类型][,参数2类型][…]);函数类型关键字函数名([参数1类型,标识符1][,参数2类型,标识符2][…]);C++中的函数原型说明了函数的类型、函数名、函数各形式参数类型。使用函数原型是C和C++的一个重要特点。3.2.2函数的声明【例3.2】对被调函数做声明的示例。#include<iostream.h>//*****ex3_2.cpp*****voidmain(){floatadd(floatx,floaty);//对add函数作声明

floatsubtract(float,float);//对subtract函数作声明,用函数原型

doublemultiply(floatp,floatq);//对multiply函数作声明,参数名任意

floata,b,c1,c2;doublec3;cout<<"pleaseinputa,b:";cin>>a>>b;c1=add(a,b);c2=subtract(a,b);c3=multiply(a,b);cout<<c1<<","<<c2<<","<<c3<<endl;}}floatadd(floatx,floaty)//定义add函数{floatz;z=x+y;return(z);}floatsubtract(floatx,floaty)//定义subtract函数{floatz;z=x-y;return(z);}doublemultiply(floatx,floaty)//定义multiply函数{doublez;z=x*y;return(z);}运行情况如下:pleaseinputa,b:4.252.00↙6.25,2.25,8.5

说明:(1)对函数的定义和函数声明是两回事,不要混淆。(2)之所以函数原型中可以省略形式参数的名称,是因为形式参数的名称是无关紧要的,且在调用前形参并不存在。(3)函数声明语句的位置。函数声明语句可以放在主调函数中,也可放在函数外面,只要出现在调用语句之前即声明有效。3.2.3函数的返回值(1)函数的返回值是通过函数中的return语句获得的。return语句将被调用函数中的一个确定值带回主调函数中去。return语句后面的括号可以要,也可以不要。return后面的值可以是一个表达式。(2)函数值的类型。既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。(3)如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准,即函数类型决定返回值的类型。对数值型数据,可以自动进行类型转换。第(1)种形式是基本的形式。为了便于阅读程序,也允许在函数原型中加上参数名,就成了第(2)种形式。但编译系统并不检查参数名。因此参数名是什么都无所谓。上面程序中的声明也可以写成floatadd(floata,floatb);//参数名不用x、y,而用a、b效果完全相同。应当保证函数原型与函数首部写法上的一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。在函数调用时函数名、实参类型和实参个数应与函数原型一致。说明:(1)前面已说明,如果被调用函数的定义出现在主调函数之前,可以不必加以声明。因为编译系统已经事先知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。有经验的程序编制人员一般都把main函数写在最前面,这样对整个程序的结构和作用一目了然,统览全局,然后再具体了解各函数的细节。此外,用函数原型来声明函数,还能减少编写程序时可能出现的错误。由于函数声明的位置与函数调用语句的位置比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。所以应养成对所有用到的函数作声明的习惯。这是保证程序正确性和可读性的重要环节。(2)函数声明的位置可以在调用函数所在的函数中,也可以在函数之外。如果函数声明放在函数的外部,在所有函数定义之前,则在各个主调函数中不必对所调用的函数再作声明。例如:charletter(char,char);//本行和以下两行函数声明在所有函数之前且在函数外部floatf(float,float);//因而作用域是整个文件inti(float,float);intmain(){…}//在main函数中不必对它所调用的函数作声明charletter(charc1,charc2)//定义letter函数{…}floatf(floatx,floaty)//定义f函数{…}inti(floatj,floatk)//定义i函数{…}如果一个函数被多个函数所调用,用这种方法比较好,不必在每个主调函数中重复声明。函数调用的一般形式:函数名([实际参数列表])如果是调用无参函数,则“实参表列”可以没有,但括号不能省略。如果实参表列包含多个实参,则各参数间用逗号隔开。实参与形参的个数应相等,类型应匹配(相同或赋值兼容)。实参与形参按顺序对应,一对一地传递数据。但应说明,如果实参表列包括多个实参,对实参求值的顺序并不是确定的。3.2.4函数的调用

按函数在语句中的作用来分,可以有以下3种函数调用方式:1.函数语句把函数调用单独作为一个语句,并不要求函数带回一个值,只是要求函数完成一定的操作。如【例3.1】中的print_word()函数调用语句。2.函数表达式函数出现在一个表达式中,这时要求函数带回一个确定的值以参加表达式的运算。如c=2*max(a,b);3.函数参数函数调用作为一个函数的实参。如m=max(a,max(b,c));//max(b,c)是函数调用,其值作为外层max函数调用的一个实参函数调用的方式3.3函数的参数传递

形参:在定义函数时函数名后面括号中的变量名称为形式参数(formalparameter),简称形参。形参是无内存单元(因而不存在)的任何合法标识符。实参:在调用一个函数时,调用语句的函数名后面括号中的参数称为实际参数(actualparameter),简称实参。实参是实际存在(因而有特定值)的常量、变量或表达式。

【例3.3】形参和实参及其数据传递。#include<iostream>//*****ex3_3.cpp*****usingnamespacestd;floatvolume(floatr,floath)//定义有参函数volume求圆柱体体积,r和h是形参{floatv;v=3.14*r*r*h;return(v);}voidmain(){floata,b,c;cout<<"pleaseinputtwofloatnumbers:";cin>>a>>b;c=volume(a,b);//调用函数volume,a和b是实参。函数值赋给变量ccout<<"Thevolumeis"<<c<<endl;}运行情况如下:pleaseinputtwofloatnukbers:2.05.0↙Thevolumeis62.8有关形参与实参的说明:(1)在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,表示它们并不是实际存在的数据,只有在发生函数调用时,函数max中的形参才被分配内存单元,以便接收从实参传来的数据。在调用结束后,形参所占的内存单元也被释放。(2)实参可以是常量、变量或表达式,如max(3,a+b);但要求a和b有确定的值。以便在调用函数时将实参的值赋给形参。(3)在定义函数时,必须在函数首部指定形参的类型,至于形参使用何名字可随意。(4)实参与形参的类型应相同或赋值兼容。两种参数类型完全一致无疑是完全合法、正确的。如果实参为整型而形参为实型,或者相反,则按不同类型数值的赋值规则进行转换,原则上不出现语法错误,但结果可能带来某些非期望的误差。例如实参a的值是3.5,而被调函数的形参x为整型,则调用该函数时会将3.5转化为整数3,然后送到形参x,故x得到的是3,由3计算出的函数值与人们期望由3.5计算出的函数值一般是有差别的。但字符型与整型可以互相通用。3.3.2参数的值传递值传递参数的实现是系统将实参拷贝一个副本给形参,拷贝后两者就断开关系。在被调函数中,形参可以被改变,但这只影响副本中的形参值,而不影响调用函数的实参值。所以这类函数有对原始数据保护的作用。换一句话说,这种参数传递机制是单向影响,即只能由实参将值传给形参(实参影响形参);而形参在函数中的值如果发生修改,不会反过来影响与之对应的实参。【例3.4】参数值传递的演示。#include<iostream>//*****ex3_4.cpp*****usingnamespacestd;intmax(intx,inty)//定义有参函数max求两数最大值,x和y是形参{floatm;cout<<"Theinitialx,y:"<<x<<","<<y<<endl;m=x>y?x:y;x=2*x;y=y+1;cout<<"Thenewx,y:"<<x<<","<<y<<endl;return(m);}voidmain(){floata,b,c;cout<<"pleaseinputtwointegernumbers:";cin>>a>>b;c=max(a,b);//调用函数max,a和b是实参,函数值赋给变量c。cout<<"Themaxinumis:"<<c<<endl;cout<<"Aftercallingfuction,a,b:"<<a<<","<<b<<endl;}运行情况如下:pleaseinputtwointegernumbers:38↙Theinitialx,y:3,8Thenewx,y:69Themaxinumis:8Aftercallingfuction,a,b:3,8}调用结束后,形参单元被释放,实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。3.3.3参数的地址传递除了3.3.2小节介绍的值传递参数方式外,函数调用还有一种特殊的值传递形式,即传递的值不是一般的数值,而是一些内存单元地址编号(即地址),这时,一般称之为参数的地址传递。在这种参数传递形式中,无论在函数的定义中出现的形参还是在调用语句中出现的实参,都是代表一些内存单元地址编号(即地址数值),而不是一般的数值。C++中的参数地址传递情况一般有如下几种:实参可以是一个有确定值的普通变量的地址,或者是一个已经初始化的指针变量;或者是一个初始化的数组名;或者是一个具体的函数名。而形参可以是一个任意普通变量的地址,或是一个任意指针变量,或是一个任意的数组名,或是一个指向函数的指针变量(对应于实参是具体函数名)。实际上,这种参数传递机制就是在函数调用时把一个内存单元地址传递给形参,使形参也具有实参的内存单元地址(即两者对应同一个内存单元),称作形参和实参地址结合,两者合二为一。这样一来,任何时候形参的值等于实参的值;而实参的值也等于形参的值。因此,形参在函数中发生变化后,也会引起实参跟着变化(因为它们是捆绑在一起的,一体化的)。这就意味着按地址传递的方式,在调用刚开始时实参的值影响了形参;而在被调函数执行过程中形参值若发生了变化,它也会影响实参的值变化。即机制是双向影响,这与普通值传递方式的单向影响机制形成对比。3.3.4带默认值的参数C++语言中,允许在函数声明或定义时给一个或多个参数指定默认值。通常调用函数时,要为函数的每个形式参数给定相应的值。例如下面的delay()函数作用是作时间延迟,在没有使用默认值参数的情况下,是按如下普通方式声明和定义的:【例3.5】延迟函数的使用。#include<iostream.h>//*****ex3_5.cpp*****voiddelay(intloop);//函数声明voidmain(){cout<<"begin"<<endl;delay(1000);//函数调用cout<<"end"<<endl;}voiddelay(intloop)//函数定义{if(loop==0)return;for(inti=0;i<loop;i++)cout<<i<<endl;//输出i的值为了清楚看到程序执行的情况}如果每次调用延迟时间基本一样,可以使用C++中默认值参数函数形式来解决问题。解决的方法就是在函数的声明(或定义)时给定默认值即可。具体做法只要把delay()函数的声明改为下列形式:voiddelay(intloop=1000);//指定参数默认值为1000以后如果需要延迟相同时间1000,都可以不必指定实参的值而直接调用函数:delay();//不给定实参,形参将得到默认值1000delay(500);//给定实参,形参将得到所给的值500如果有多个形参,可以使每个形参有一个默认值;也可以只对一部分形参指定默认值。如前面的求圆柱体体积的函数volume,可以这样声明:floatvolume(floatr,floath=8.5);//只对形参h指定默认值8.5这时函数调用可采用以下形式:volume(6.0);//相当于volume(6.0,8.5)volume(6.0,7.2);//r的值为6.0,h的值为7.2C++中实参和形参的结合是从左至右进行的,第1个实参必然与第1个形参结合,第2个实参必然与第2个形参结合,……。因此,指定默认值的参数必须放在参数列表中的最右边。3.4函数的嵌套调用和递归调用

C++不允许对函数作嵌套定义,也就是说在一个函数中不能完整地包含另一个函数。在一个程序中每一个函数的定义都是互相平行和独立的。虽然C++不能嵌套定义函数,但可以嵌套调用函数,也就是说,在调用一个函数的过程中,又调用另一个函数。见图3.2示意。

图3.2在程序中实现函数嵌套调用时,需要注意的是:在调用函数之前,需要对每一个被调用的函数作声明(除非定义在前,调用在后)。【例3.6】编程求组合,其中求组合的功能要求用函数完成。分析:根据组合的计算公式,知组合函数有两个形参:m和n,可以用自定义函数comb(intn,intm)表示求组合。而在comb函数中需要3次计算阶乘,如果定义函数fac(k)求k的阶乘,然后在comb函数中调用fac函数,可以使程序代码简单,只要在comb函数中写一个语句“c=fac(m)/(fac(n)*fac(m-n));”即可求出组合值。【例3.6】程序代码如下:#include<iostream>//*****ex3_6.cpp*****usingnamespacestd;longfac(intk)//定义求阶乘的函数{longf=1;inti;for(i=1;i<=k;i++)f=f*i;returnf;}longcomb(intn,intm)//定义组合函数{longc;c=fac(m)/(fac(n)*fac(m-n));//嵌套调用阶乘函数

returnc;}voidmain(){intn,m;longc;cout<<"pleaseinputtwointegernumbers:m,n"<<endl;cin>>m>>n;c=comb(n,m);//调用组合函数combcout<<"c="<<c<<endl;}主函数调用函数comb();comb()在执行过程中又调用了函数fac()。fac()的调用被嵌套在函数comb()的调用中。在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归(recursive)调用。C++允许函数的递归调用。例如:直接递归调用的代码形式如下:intf1()//函数f1的定义{……//函数其他部分

z=f1();//直接调用自身

……//函数其他部分}以上是在函数f1()中,又直接调用了f1()函数,直接递归调用过程如图3.3所示。3.4.2函数的递归调用间接递归调用可以表现为如下形式:intf2()//函数f2的定义{……//f2的其他部分x=f3();//调用f3()……//f2的其他部分}intf3()//函数f3的定义{……//f3的其他部分y=f2();//调用f2()……//f3的其他部分}函数f2()中调用了f3(),而f3()中又调用了f2(),相当于f2()间接地调用了f2()。这种调用称为间接递归调用,调用过程如图3.4所示。

图3.3函数的直接递归调用图3.4函数的间接递归调用从图上可以看到,这两种递归调用都是无终止的自身调用。显然,程序中不应出现这种无终止的递归调用,而只应出现有限次数的、有终止的递归调用,这可以用if语句来控制,只有在某一条件成立时才继续执行递归调用,否则就不再继续。包含递归调用的函数称为递归函数。【例3.7】用递归计算n!。分析:n!本身就是以递归的形式定义的:求n!,应先求(n-1)!;而求(n-1)!,又需要先求(n-2)!,而求(n–2)!;又可以变成求(n-3)!,如此继续,直到最后变成求0!的问题,而根据公式有0!=1(这就是本问题的递归终止条件)。由终止条件得到0!结果后,再反过来依次求出1!,2!……直到最后求出n!。设求n!的函数为fac(n),函数体内求n!,只要n>0,可用n*fac(n-1)表示,即fac(n)的函数体内将递归调用fac()本身;但一旦参数n为0时,则终止调用函数自身并给出函数值1。程序如下:usingnamespacestd;#include<iostream>//*****ex3_7.cpp*****longfac(intn){longf;if(n==0)f=1;elsef=n*fac(n-1);//递归调用,求(n-1)!returnf;}voidmain(){longy;intn;cout<<"pleaseinputaintegern"<<endl;cin>>n;y=fac(n);//调用fac(n)求n!cout<<"n="<<n<<","<<"y="<<y<<endl;}运行时,如果输入:3,运行结果如下:n=3,y=6递归调用及返回过程如图3.5所示,图中的数字序号表示递归调用和返回的先后顺序。图3.5求3!的递归过程fac(3)fac()n=33*fac(2)return6main()fac()n=22*fac(1)return2n=0fac(0)=1return1n1*fac(0)return1fac()fac()⑦②①③④⑧⑥⑤从求n!的递归程序中可以看出,递归定义有两个要素:(1)递归终止条件。也就是所描述问题的最简单情况,它本身不再使用递归的定义,即程序必须终止。如上例,当n=0时,fac(n)=1,不再使用f(n-1)来定义。(2)递归定义使问题向终止条件转化的规则。递归定义必须能使问题越来越简单,即参数越来越接近终止条件的参数;达到终止条件参数时函数有确定的值。如上例,f(n)由f(n-1)定义,越来越靠近f(0),即参数越来越接近终止条件参数0;达到终止条件参数时函数有确定的值是f(0)=1。【例3.8】汉诺塔问题汉诺塔(TowerofHanoi)问题据说来源于布拉玛神庙。该问题的装置如图3.6所示(图上仅画三个金片以简化问题的原理,原问题有64个金片),底座上有三根金钢石的针,第一根针a上放着从大到小64个金片。解决该问题就是要想法把所有金片从第一根针a上移到第三根针c上,第二根针b作为中间过渡。要求是每次只能移动一个金片,并且任何时候不允许大的金片压在小的金片上面。图3.6三个金片的汉诺塔问题装置abc1.本问题的递归终止条件。如果只有1个盘,显然问题的解就很明显是:直接把金片从a移到c。因此终止条件是n=1;终止条件对应的操作是直接把金片从a移到c,示意ac。2.本问题的递归分析:移动n个金片从a到c,必须先将n-1个金片从a借助c移动到b,移动n-1个金片与原问题相同,但规模变小,即向终止条件接近,因此,此问题可以用递归过程完成。递归过程可以用如下步骤表示:(1)将n-1个金片从a经过c移动到b。(2)将第n个金片从a直接移动到c。(3)再将n-1个金片从b经过a移动到c。

一般地,设将n个金片从x针借助y针移动到z针的函数原形为:voidhanoi(intn,charx,chary,charz)根据解题步骤,可以写出求解n个金片的汉诺塔函数如下:#include<iostream>//*****ex3_8.cpp*****usingnamespacestd;voidhanoi(intn,charx,chary,charz){if(n==1)//n=1时,直接将金片从x移动到zcout<<x<<"->"<<z<<endl;else//n>1时{hanoi(n-1,x,z,y);//先将n-1个金片从借助z移动到ycout<<x<<"->"<<z<<endl;//然后将第n个金片从x移到zhanoi(n-1,y,x,z);//再将n-1个金片从y借助x移动到z}}当n>1时,就递归调用hanoi(),每次n减1。最后当n=1时,直接移动该金片就可以了。主函数如下:

voidmain(){intn;cout<<"inputn:"<<endl;cin>>n;hanoi(n,'a','b','c');//n个金片从a针借助b针移动到c针}虽然递归调用在写程序时很简单,但执行起来却很复杂(时间、存储空间都开销大)。对于汉诺塔问题程序的执行过程分析比较复杂,有兴趣的读者可参阅教材对3个盘情景的分析(图3.7及其相应文字叙述)。调用函数时需要一定的时间和空间的开销。下图表示的是一般函数调用的过程。一般函数调用过程3.5内置函数3.5.1内置函数的作用内置函数也称内联函数、内嵌函数。引入内置函数的目的是为了提高程序中函数调用的效率。C++提供一种提高效率的方法,即在编译时将所调用函数的代码直接嵌入到主调函数中,而不是将流程转出去。这种嵌入到主调函数中的函数称为内置函数(inlinefunction),又称内嵌函数。在有些书中把它译成内联函数。指定内置函数的方法很简单,只需在函数首行的左端加一个关键字inline即可。【例3.9】判断用户从键盘输入的系列字符是数字字符还是其它字符的函数is_number()。#include<iostream.h>//*****ex3_9a.cpp*****intis_number(char);//函数声明voidmain(){charc;while((c=cin.get())!='\n'){if(is_number(c))//调用一个小函数cout<<"youenteradigit\n";elsecout<<"youenteranon-digit\n";}}intis_number(charch)//函数定义{return(ch>='0'&&ch<='9')?1:0;}程序中不断到设备中读取数据,频繁调用is_number()函数。为了避免频繁调用函数,提高执行效率,可以将【例3.9】程序改为:#include<iostream.h>//*****ex3_9b.cpp*****voidmain(){charc;while((c=cin.get())!='\n'){if((c>='0'&&c<='9')?1:0)//修改处:直接计算表达式

cout<<"youenteradigit\n";elsecout<<"youenteranon-digit\n";}}修改后的程序在if语句中用表达式替换了函数调用。在程序运行上,提高了一些执行效率,因为免去了大量的函数调用开销。但是,由于is_number函数比相应的表达式可读性好,所以修改后的代码可读性降低,尤其若程序中多处出现is_number的替换时,会大大降低可读性。我们希望既要用函数调用来体现其结构化和可读性,又要使效率尽可能地高。为了尽量做到两全其美,C++中引入内置函数这个方法。3.5.2定义和使用内置函数内置函数的定义格式如下:inline函数名(形参列表){……//函数体}内置函数的声明格式如下:inline函数名(形参类型表);其实,内置函数只要在开头一次性声明为inline即可,而后面的函数定义仍可写成一般函数定义的形式,编译器也会将函数视为内置函数。对【例3.9】使用内置函数来解决,代码可以写成下列形式:#include<iostream.h>//*****ex3_9c.cpp*****inlineintis_number(char);//inline函数声明voidmain(){charc;while((c=cin.get())!='\n'){if(is_number(c))//调用一个小函数cout<<"youenteradigit\n";elsecout<<"youenteranon-digit\n";}}intis_number(charch)//此处无inline,却被本程序视为inline{return(ch>='0'&&ch<='9')?1:0;}关于内置函数的说明:(1)内置函数与一般函数的区别在于函数调用的处理。一般函数进行调用时,要将程序执行到被调用函数中,然后返回到主调函数中;而内置函数在调用时,是将调用部分用内置函数体来替换。(2)内置函数必须先声明后调用。因为程序编译时要对内置函数替换,所以在内置函数调用之前必须声明是内置的(inline),否则将会像一般函数那样产生调用而不是进行替换操作。(3)在内置函数中,不能含有复杂的结构控制语句,如switch、for和while。如果内置函数有这些语句,则编译器将该函数视同一般函数那样产生函数调用。(4)递归函数不能用作内置函数。(5)以后讲到的类中,所有定义在说明内部的函数都是内置函数。归纳起来,只有那些规模较小而又被频繁调用的简单函数,才适合于声明为inline函数。在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的。同样,在复合语句中定义的变量只在本复合语句范围内有效。这称为局部变量(localvariable)。如局部变量和全局变量

局部变量floatf1(inta) //函数f1{intb,c; b、c有效 a有效┆}charf2(intx,inty) //函数f2{inti,j; i、j有效 x、y有效┆}intmain() //主函数{intm,n;┆{intp,q; p、q在复合语句中有效 m、n有效┆}}说明:(1)主函数main中定义的变量(m,n)也只在主函数中有效,不会因为在主函数中定义而在整个文件或程序中有效。主函数也不能使用其他函数中定义的变量。(2)不同函数中可以使用同名的变量,它们代表不同的对象,互不干扰。例如,在f1函数中定义了变量b和c,倘若在f2函数中也定义变量b和c,它们在内存中占不同的单元,不会混淆。(3)可以在一个函数内的复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也称为分程序或程序块。(4)形式参数也是局部变量。例如f1函数中的形参a也只在f1函数中有效。其他函数不能调用。(5)在函数声明中出现的参数名,其作用范围只在本行的括号内。实际上,编译系统对函数声明中的变量名是忽略的,即使在调用函数时也没有为它们分配存储单元。例如intmax(inta,intb);//函数声明中出现a、b┆intmax(intx,inty)//函数定义,形参是x、y{cout<<x<<y<<endl;//合法,x、y在函数体中有效cout<<a<<b<<endl;//非法,a、b在函数体中无效}编译时认为max函数体中的a和b未经定义。前面已介绍,程序的编译单位是源程序文件,一个源文件可以包含一个或若干个函数。在函数内定义的变量是局部变量,而在函数之外定义的变量是外部变量,称为全局变量(globalvariable,也称全程变量)。全局变量的有效范围为从定义变量的位置开始到本源文件结束。如全局变量intp=1,q=5;//全局变量 全局变量c1、c2的作用范围floatf1(a)//定义函数f1inta;{intb,c;┆}charc1,c2;//全局变量 全局变量p、q的作用范围charf2(intx,inty)//定义函数f2{inti,j;┆}main()//主函数{intm,n;┆}p、q、c1、c2都是全局变量,但它们的作用范围不同,在main函数和f2函数中可以使用全局变量p、q、c1、c2,但在函数f1中只能使用全局变量p、q,而不能使用c1和c2。在一个函数中既可以使用本函数中的局部变量,又可以使用有效的全局变量。说明:(1)设全局变量的作用是增加函数间数据联系的渠道。(2)建议不在必要时不要使用全局变量,因为:①全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。②它使函数的通用性降低了,因为在执行函数时要受到外部变量的影响。如果将一个函数移到另一个文件中,还要将有关的外部变量及其值一起移过去。但若该外部变量与其他文件的变量同名,就会出现问题,降低了程序的可靠性和通用性。在程序设计中,在划分模块时要求模块的内聚性强、与其他模块的耦合性弱。即模块的功能要单一(不要把许多互不相干的功能放到一个模块中),与其他模块的相互影响要尽量少,而用全局变量是不符合这个原则的。一般要求把程序中的函数做成一个封闭体,除了可以通过“实参——形参”的渠道与外界发生联系外,没有其他渠道。这样的程序移植性好,可读性强。③使用全局变量过多,会降低程序的清晰性。在各个函数执行时都可能改变全局变量的值,程序容易出错。因此,要限制使用全局变量。(3)如果在同一个源文件中,全局变量与局部变量同名,则在局部变量的作用范围内,全局变量被屏蔽,即它不起作用。变量的有效范围称为变量的作用域(scope)。归纳起来,变量有4种不同的作用域、文件作用域(filescope)、函数作用域(functionscope)、块作用域(blockscope)和函数原型作用域(functionprototypescope)。文件作用域是全局的,其他三者是局部的。除了变量之外,任何以标识符代表的实体都有作用域,概念与变量的作用域相似。【例3.12】分别写两个函数求给定两个数的最大公约数和最小公倍数。其中,要求用全局变量存放最大公约数和最小公倍数。分析:由于求最小公倍数要依赖于最大公约数,所以应先求最大公约数。故应将求最大公约数的函数写在前面,求最小公倍数的函数放在后面。#include<iostream.h>//*****ex3_12.cpp*****intgreatest_common_divisor(int,int);//声明求最大公约数的函数intlease_common_multiple(int,int);//声明求最小公倍数的函数intmax,min;//全局变量分别存放最大公约数、最小公倍数voidmain(){inta;intb;cout<<"inputa,b:";cin>>a>>b;greatest_common_divisor(a,b);lease_common_multiple(a,b);cout<<"thegreatestcommondivisoris:"<<max<<endl;//使用全局变量maxcout<<"theleasecommonmultipleis:"<<min<<endl;//使用全局变量min}intgreatest_common_divisor(intx,inty){intt;intr;if(x<y){t=x;x=y;y=t;}r=x%y;while(r!=0){x=y;y=r;r=x%y;}max=y;returnmax;//使用全局变量max}intlease_common_multiple(intx,inty){min=x*y/max;//使用全局变量max求全局变量min,returnmin;//返回全局变量min}程序运行结果如下:inputa,b:3615↙thegreatestcommondivisoris:3theleasecommonmultipleis:180本例中利用全局变量max和min存储最大公约数和最小公倍数。最大公约数是在函数greatest_common_divisor()中赋给max的;最小公倍数是在函数lease_common_multiple()中赋给min的,而max和min又在main()函数中使用。本程序的整个过程就是利用全局变量max和min由greatest_common_divisor()函数和lease_common_multiple()函数向main()函数传递数据实现的。【例3.13】函数中变量和程序块中变量重名#include<iostream.h>//*****ex3_13.cpp*****voidmain(){inta=1,b=2,c=3;cout<<a<<","<<b<<","<<c<<endl;{intb=4;cout<<a<<","<<b<<","<<c<<endl;a=b;{intc;c=b;cout<<a<<","<<b<<","<<c<<endl;}cout<<a<<","<<b<<","<<c<<endl;}cout<<a<<","<<b<<","<<c<<endl;}程序运行结果如下:1,2,31,4,34,4,44,4,34,2,3

【例3.14】全局变量和局部变量重名#include<iostream.h>//*****ex3_14.cpp*****voidfun1();floatx=3.5;voidmain(){floatx;cout<<"inputadata:";cin>>x;cout<<"xinmainis:"<<x<<endl;fun1();cout<<"xinmainis:"<<x<<endl;}voidfun1(){cout<<"xinfun1is:"<<x<<endl;}运行结果:inputadata:6.6↙xinmainis:6.6xinfun1is:3.5xinmainis:6.6上一节已介绍了变量的一种属性——作用域,作用域是从空间的角度来分析的,分为全局变量和局部变量。变量还有另一种属性——存储期(storageduration,也称生命期)。存储期是指变量在内存中的存在期间。这是从变量值存在的时间角度来分析的。存储期可以分为静态存储期(staticstorageduration)和动态存储期(dynamicstorageduration)。这是由变量的静态存储方式和动态存储方式决定的。3.6.2变量的存储类别

动态存储方式与静态存储方式所谓静态存储方式是指在程序运行期间,系统对变量分配固定的存储空间。而动态存储方式则是在程序运行期间,系统对变量动态地分配存储空间。先看一下内存中的供用户使用的存储空间的情况。这个存储空间可以分为三部分,即:(1)程序区(2)静态存储区(3)动态存储区图4.13数据分别存放在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储单元,程序执行完毕就释放这些空间。在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。在动态存储区中存放以下数据:①函数形式参数。在调用函数时给形参分配存储空间。②函数中的自动变量(未加static声明的局部变量,详见后面的介绍)。③函数调用时的现场保护和返回地址等。对以上这些数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的,如果在一个程序中两次调用同一函数,则要进行两次分配和释放,而两次分配给此函数中局部变量的存储空间地址可能是不相同的。如果在一个程序中包含若干个函数,每个函数中的局部变量的存储期并不等于整个程序的执行周期,它只是整个程序执行周期的一部分。根据函数调用的情况,系统对局部变量动态地分配和释放存储空间。在C++中变量除了有数据类型的属性之外,还有存储类别(storageclass)的属性。存储类别指的是数据在内存中存储的方法。存储方法分为静态存储和动态存储两大类。具体包含4种:自动的(auto)、静态的(static)、寄存器的(register)和外部的(extern)。根据变量的存储类别,可以知道变量的作用域和存储期。函数中的局部变量,如果不用关键字static加以声明,编译系统对它们是动态地分配存储空间的。函数的形参和在函数中定义的变量(包括在复合语句中定义的变量)都属此类。在调用该函数时,系统给形参和函数中定义的变量分配存储空间,数据存储在动态存储区中。在函数调用结束时就自动释放这些空间。如果是在复合语句中定义的变量,则在变量定义时分配存储空间,在复合语句结束时自动释放空间。因此这类局部变量称为自动变量(autovariable)。自动变量用关键字auto作存储类别的声明。例如:◆自动变量intf(inta)//定义f函数,a为形参{autointb,c=3;//定义b和c为整型的自动变量┆}存储类别auto和数据类型int的顺序任意。关键字auto可以省略,如果不写auto,则系统把它默认为自动存储类别,它属于动态存储方式。程序中大多数变量属于自动变量。本书前面各章所介绍的例子中,在函数中定义的变量都没有声明为auto,其实都默认指定为自动变量。在函数体中以下两种写法作用相同:①autointb,c=3;②intb,c=3;有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,在下一次该函数调用时,该变量保留上一次函数调用结束时的值。这时就应该指定该局部变量为静态局部变量(staticlocalvariable)。◆用static声明静态局部变量例:静态局部变量的值。#include<iostream>usingnamespacestd;intf(inta)//定义f函数,a为形参{autointb=0;//定义b为自动变量

staticintc=3;//定义c为静态局部变量

b=b+1;c=c+1;returna+b+c;}

intmain(){inta=2,i;for(i=0;i<3;i++)cout<<f(a)<<″″;cout<<endl;return0;}运行结果为789先后3次调用f函数时,b和c的值如书中表4.1所示。图4.14对静态局部变量的说明:(1)静态局部变量在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,存储在动态存储区空间(而不是静态存储区空间),函数调用结束后即释放。(2)为静态局部变量赋初值是在编译时进行值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而为自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。(3)如果在定义局部变量时不赋初值的话,对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符型变量)。而对自动变量来说,如果不赋初值,则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。(4)虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的,也就是说,在其他函数中它是“不可见”的。在什么情况下需要用局部静态变量呢?(1)需要保留函数上一次调用结束时的值。例如可以用下例中的方法求n!。例:输出1~5的阶乘值(即1!,2!,3!,4!,5!)。#include<iostream>usingnamespacestd;intfac(int);//函数声明intmain(){inti;for(i=1;i<=5;i++)cout<<i<<″!=″<<fac(i)<<endl;return0;}intfac(intn){staticintf=1;//f为静态局部变量,函数结束时f的值不释放

f=f*n;//在f原值基础上乘以nreturnf;}运行结果为1!=12!=23!=64!=245!=120每次调用fac(i),就输出一个i,同时保留这个i!的值,以便下次再乘(i+1)。(2)如果初始化后,变量只被引用而不改变其值,则这时用静态局部变量比较方便,以免每次调用时重新赋值。但是应该看到,用静态存储要多占内存,而且降低了程序的可读性,当调用次数多时往往弄不清静态局部变量的当前值是什么。因此,如不必要,不要多用静态局部变量。一般情况下,变量的值是存放在内存中的。当程序中用到哪一个变量的值时,由控制器发出指令将内存中该变量的值送到CPU中的运算器。经过运算器进行运算,如果需要存数,再从运算器将数据送到内存存放。如图4.15所示。图4.15◆用register声明寄存器变量为提高执行效率,C++允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。这种变量叫做寄存器变量,用关键字register作声明。例如,可以将例4.14中的fac函数改写如下:

intfac(intn){registerinti,f=1;//定义i和f是寄存器变量

for(i=1;i<=n;i++)f=f*i;returnf;}定义f和i是存放在寄存器的局部变量,如果n的值大,则能节约许多执行时间。在程序中定义寄存器变量对编译系统只是建议性(而不是强制性)的。当今的优化编译系统能够识别使用频繁的变量,自动地将这些变量放在寄存器中。全局变量(外部变量)是在函数的外部定义的,它的作用域为从变量的定义处开始,到本程序文件的末尾。在此作用域内,全局变量可以为本文件中各个函数所引用。编译时将全局变量分配在静态存储区。有时需要用extern来声明全局变量,以扩展全局变量的作用域。◆用extern声明外部变量1.在一个文件内声明全局变量如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件终了。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字extern对该变量作外部变量声明,表示该变量是一个将在下面定义的全局变量。有了此声明,就可以从声明处起,合法地引用该全局变量,这种声明称为提前引用声明。例:用extern对外部变量作提前引用声明,以扩展程序文件中的作用域。#include<iostream>usingnamespacestd;intmax(int,int);//函数声明voidmain(){externinta,b;//对全局变量a,b作提前引用声明

cout<<max(a,b)<<endl;}inta=15,b=-7;//定义全局变量a,bintmax(intx,inty){intz;z=x>y?x:y;returnz;}运行结果如下:15在main后面定义了全局变量a,b,但由于全局变量定义的位置在函数main之后,因此如果没有程序的第5行,在main函数中是不能引用全局变量a和b的。现在我们在main函数第2行用extern对a和b作了提前引用声明,表示a和b是将在后面定义的变量。这样在main函数中就可以合法地使用全局变量a和b了。如果不作extern声明,编译时会出错,系统认为a和b未经定义。一般都把全局变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个extern声明。2.在多文件的程序中声明外部变量如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量num,不能分别在两个文件中各自定义一个外部变量num。正确的做法是:在任一个文件中定义外部变量num,而在另一文件中用extern对num作外部变量声明。即externintnum;编译系统由此知道num是一个已在别处定义的外部变量,它先在本文件中找有无外部变量num,如果有,则将其作用域扩展到本行开始(如上节所述),如果本文件中无此外部变量,则在程序连接时从其他文件中找有无外部变量num,如果有,则把在另一文件中定义的外部变量num的作用域扩展到本文件,在本文件中可以合法地引用该外部变量num。分析下例:file1.cppfile2.cppexterninta,b;inta=3,b=4;intmain()┆{cout<<a<<″,″<<b<<endl;return0;}用extern扩展全局变量的作用域,虽然能为程序设计带来方便,但应十分慎重,因为在执行一个文件中的函数时,可能会改变了该全局变量的值,从而会影响到另一文件中的函数执行结果。有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个static声明。例如:file1.cppfile2.cppstaticinta=3;externinta;intmain()intfun(intn){{┆┆a=a*n;┆}}◆用static声明静态外部变量这种加上static声明、只能用于本文件的外部变量(全局变量)称为静态外部变量。这就为程序的模块化、通用性提供了方便。如果已知道其他文件不需要引用本文件的全局变量,可以对本文件中的全局变量都加上static,成为静态外部变量,以免被其他文件误用。需要指出,不要误认为用static声明的外部变量才采用静态存储方式(存放在静态存储区中),而不加static的是动态存储(存放在动态存储区)。实际上,两种形式的外部变量都用静态存储方式,只是作用范围不同而已,都是在编译时分配内存的。一个变量除了数据类型以外,还有3种属性:(1)存储类别C++允许使用auto,static,register和extern4种存储类别。(2)作用域指程序中可以引用该变量的区域。(3)存储期指变量在内存的存储期限。以上3种属性是有联系的,程序设计者只能声明变量的存储类别,通过存储类别可以确定变量的作用域和存储期。要注意存储类别的用法。auto,static和register3种存储类别只能用于变量的定义语句中,如变量属性小结autocharc;//字符型自动变量,在函数内定义staticinta;//静态局部整型变量或静态外部整型变量registerintd;//整型寄存器变量,在函数内定义externintb;//声明一个已定义的外部整型变量说明:extern只能用来声明已定义的外部变量,而不能用于变量的定义。只要看到extern,就可以判定这是变量声明,而不是定义变量的语句。下面从不同角度分析它们之间的联系。(1)从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:●局部变量自动变量,即动态局部变量(离开函数,值就消失)静态局部变量(离开函数,值仍保留)寄存器变量(离开函数,值就消失)形式参数(可以定义为自动变量或寄存器变量)●全局变量静态外部变量(只限本文件引用)外部变量(即非静态的外部变量,允许其他文件引用)(2)从变量存储期(存在的时间)来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。●动态存储自动变量(本函数内有效)寄存器变量(本函数内有效)形式参数●静态存储静态局部变量(函数内有效)静态外部变量(本文件内有效)外部变量(其他文件可引用)(3)从变量值存放的位置来区分,可分为●内存中静态存储区静态局部变量静态外部变量(函数外部静态变量)外部变量(可为其他文件引用)●内存中动态存储区:自动变量和形式参数●CPU中的寄存器:寄存器变量(4)关于作用域和存储期的概念。从前面叙述可以知道,对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量值存在时间的长短,即存储期。前者是从空间的角度,后者是从时间的角度。二者有联系但不是同一回事。图4.16是作用域的示意图,图4.17是存储期的示意图。

图4.16图4.17如果一个变量在某个文件或函数范围内是有效的,则称该文件或函数为该变量的作用域,在此作用域内可以引用该变量,所以又称变量在此作用域内“可见”,这种性质又称为变量的可见性,例如图4.16中变量a、b在函数f1中可见。如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的存储期,或称该变量在此时刻“存在”。书中表4.2表示各种类型变量的作用域和存在性的情况。可以看到自动变量和寄存器变量在函数内的可见性和存在性是一致的。在函数外的可见性和存在性也是一致的。静态局部变量在函数外的可见性和存在性不一致。静态外部变量和外部变量的可见性和存在性是一致的。(5)static声明使变量采用静态存储方式,但它对局部变量和全局变量所起的作用不同。对局部变量来说,static使变量由动态存储方式改变为静态存储方式。而对全局变量来说,它使变量局部化(局部于本文件),但仍为静态存储方式。从作用域角度看,凡有static声明的,其作用域都是局限的,或者局限于本函数内(静态局部变量),或者局限于本文件内(静态外部变量)。用static来声明一个变量的作用有二:(1)对局部变量用static声明,使该变量在本函数调用结束后不释放,整个程序执行期间始终存在,使其存储期为程序的全过程。(2)全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。请注意,用auto,register,static声明变量时,是在定义变量的基础上加上这些关键字,而不能单独使用。如“statica;”是不合法的,应写成“staticinta;”。函数按其存储类别也可以分为两类:1、内部函数2

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论