📄 03章 函数.txt
字号:
Player rolled 5 + 2 = 7
Player loses
图 3.11 投骰子游戏程序示例的执行结果
注意,游戏者首先要投两枚骰子,后面也是。我们定义rollDice函数投骰子、计算并打印点数和。函数rollDice定义一次,但可从程序的两个地方调用。有趣的是,rollDice不取参数,因此在参数表中用void表示。函数rollDice返回投两枚骰子的点数和,因此在函数首部定义的返回类型为int。
这个游戏相当复杂。游戏者第一次投两枚骰子时可能输也可能赢,也可能投好几次才会定出输赢。变量gameStatus跟踪这个状态,将其声明为Status类型。下列语句:
enum Status{ CONTIEUE,WON,LOST);
生成用户自定义类型(user-defined type)即枚举类型(enume ration)枚举类型由关键字enum和类型名(这里是Status)构成,是—组用标识符表示的整数常量。这些枚举常量(enume ration constant)的值从0开始,增量为1,但也可以指定其他的增量值。在上述枚举中,CONTINUE指定为数值0,WON指定为数值1,LOST指定为数值2。enum中的标识符必须惟一,但不同枚举常量可以取相同的值。
编程技巧3.7
作为用户自定义类型名的标识符,其第一个字母应大写。
用户自定义类型Status的变量只能赋给枚举中声明的三个值之一。游戏获胜时,gameStatus设置为WON;游戏失败时,gameStatus设置为LOST;否则gameStatus设置为CONTINUE.可以再次投骰子。
常见编程错误3.17
对枚举类型变量指定等价于枚常量的整数值是个语法错误。
另—个常用枚举是:
enum Months {JAN = 1,FEB,MAR,APR, MAY,JUN, JUL,AUG,SEP,OCT,NOV,DEC};
生成用户自定义类型Months,用枚举常量表示一年的月份。由于上述枚举中第一个值显式指定为1,因此其余值每次递增1,取值为1到12。任何枚举常量可以在枚举定义中指定一个整数值,后面的值用1递增。
常见编程错误3.18
定义枚举常量之后,想对枚举常量指定另一个值是个语法错误。
编程技巧3.8
枚举常量名用大写字母能使枚举常量在程序中更醒目,使程序更注意到枚举常量不是变量。
编程技巧3.9
用枚举而不用整数常量能使程序更清晰。
第一次投骰子之后,如果游戏获胜,则跳过while结构体,因为gameStatus不等于CONTINUE。
程序进入if/else结构,在gameStatus等于WON时打印“Playerwins",在gameStatus等于LOST时打印“Playerloses"。
第一次投骰子之后,如果游戏没有结束,则sum保存在myPoint中,由于gameStatus等于CONTINUE,因此执行while结构中的程序。每次执行while结构中的程序时,调用rollDice产生新的sum。如果sum符合myPoint,则gameStatus设置为WON,while测试失败,if/else结构打印“Playerwins”,终止执行。如果sum等于7,则gameStatus设置为LOST,while测试失败,if/el~结构打印“Playerloses”,终止执行。
注意其中使用了前面介绍的各种程序控制机制。投骰子程序使用两个函数main和rollDice,并使用switch、while、if/else和嵌套if结构。练习中,我们要介绍投骰子程序的各种有趣的特点。
3.10 存储类
第1章到第3章用标识符作为变量名。变量属性包括名称、类型、长度和值。本章用标识符作为用户自定义的函数名。实际上,程序中的每个标识符还有其他属性,包括存锗类(storageclass)、作用域(scope)和连接(linkage)。
C++提供了4个存储类说明符(storage class specifier):auto、register、extern和static。标识符的存储类说明符可以确定其存储类、范围和连接。
标识符的存储类确定了标识符在内存中存在的时间。有些标识符的存在时间很短,有些则重复生成和删除,有些存在于整个程序的执行期间。
标识符的作用域是程序中能引用这个标识符的区域。有些标识符可以在整个程序中引用,而有些标识符只能在程序中的有限部分引用。
标识符的连接确定多源文件程序(第6章将会讨论)中,只有当前源文件或是在任何正确声明的源文件中识别标识符。
本节介绍4个存储类说明符和两个存储类。3.11节介绍标识符的作用域。
存储类说明符可以分为两个存储类:自动存储类(autmatic storage class)和静态存储类(static storage class)。关键字auto和regtster用来声明自动存储类变量。这种变量在进入声明的块时生成,在块活动期间存在,在退出这个块时删除。
只有变量能作为自动存储类。函数的局部变量和参数通常是自动存储类。存储类说明符auto显式声明变量为自动存储类。例如,下列声明表示float变量x和y是自动存储类的局部变量,即只在定义该变量的函数体中存在:
auto float x,y;
局部变量默认为自动存储类,因此关键字auto很少使用。本书余下部分将自动存储类变量简称为自动变量。
性能提示3.3
自动存储可以节省内存,因为自动存储类变量在进入声明的块时生成并在退出这个块时删除。
软件工程视点3.11
自动存储是最低权限原则的例子。变量不用时没有必要放在内存中。
机器语言版本中的数据通常装入寄存器(register)中进行计算和其他处理。
性能提示3.4
存储类说明符register,可以放在自动变量声明之前,让编译器在计算机的高速硬件寄存嚣中而不是内存中保存这个变量。如果能在硬件寄存器中保存计数器、总和等大量使用的变量.则可以消除从内存向寄存器装入变量和将结果返回内存中的重复开销。
常见编程错误3.19
一个标识符使用多个存储类说明符是个语法错误,一个标识符只能使用一个存储类说明符。例如,如果一个标识符设为register,就不能再设为auto。
编译器可以忽略register声明。例如,编译器可用的寄存器个数可能不足。下列声明建议将counter变量放在计算机的寄存器中,不管编译器是否这么做,counter都初始化为1:
register int countcr = 1;
register关键字只能用于局部变量和函数参数。
性能提示3. 5
register声明通常是不需要的。如今的优化编译器通常能识别经常使用的变量,并决定将其教在寄存器中而不需要程序员进行register声明。
关键字extern和static是用来声明静态存储类变量和函数的标识符。这种变量从程序开始执行时就存在。对于变量,程序开始执行时就分配和初始化存储空间;对于函数,从程序开始执行时就存在函数名。但是,尽管变量和函数名从程序开始执行时起就存在,但这并不是说这些标识符可以在整个程序中使用。3.11节将会介绍存储类和作用域是两个不同的概念。
静态存储类有两种标识符:外部标识符(如全局变量和函数名)与存储类说明符Static中声明的局部变量。全局变量和函数名默认为存储类说明符extern。全局变量生成时将变量声明放在任何函数定义之外.在整个程序执行期间保存该全局变量的值。全局变量和函数可以由文件中已声明或定义的任何函数引用。
软件工程视点1.12
将变量声明为全局变量而不是局部变量可能发生意料不到的副作用,不需要访问该变量的函数可能有意或意外修改这个变量,一般来说,除了有独特性能要求,否则应避免使用全局变量。
软件工程视点3.13
只在某个函数中使用的变量应声明为该函数中的局都变量,而不是声明为全局变量。
用关键字static声明的局部变量仍然只在定义该变量的函数中使用,但与自动变量不同的是,static局部变量在函数退出时保持其数值。下次调用这个函数时,static局部变量包含上次函数退出时的值。下列语句将局部变量count声明为static,并将其初始化为1。
static int count = 1
所有静态存储类的数字变量默认初始化为0,但也可以由程序员显式初始化(第5章介绍的静态指针变量也是初始化为0)。
存储类说明符extern和static在显式作用于外部标识符时具有特殊意义。第18章将介绍说明符extern和static作用于外部标识符和多源文件程序。
3.11 作用域规则
程序中一个标识符有意义的部分称为其作用域。例如,块中声明局部变量时,其只能在这个块或这个块嵌套的块中引用。一个标识符的4个作用域是函数范围(function scope)、文件范围(filescope)、块范围(block scope)和函数原型范围(function-prototype scope)。后面还要介绍第五个——类范围(class scope)。
任何函数之外声明的标识符取文件范围。这种标识符可以从声明处起到文件末尾的任何函数中访问。全局变量、任何函数之外声明的函数定义和函数原型都取文件范围。
标号(后面带冒号的标识符,如start:)是惟一具有函数范围的标识符。标号可以在所在函数中任何地方使用,但不能在函数体之外引用。标号用于switch结构中(如case标号)和goto语句中(见第18章)。标号是函数内部的实现细节,这种信息隐藏(infomation hiding)是良好软件工程的基本原则之一。
块中声明的标识符的作用域为块范围。块范围从标识符声明开始,到右花括号(})处结束。函数开头声明的局部变量的作用域为块范围,函数参数也是,它们也是函数的局部变量。任何块都可以包含变量声明。块嵌套时,如果外层块中的标识符与内层块中的标识符同名,则外层块中的标识符“隐藏”,直到内层块终止。在内层块中执行时,内层块中的标识符值是本块中定义的,而不是同名的外层标识符值。声明为static的局部变量尽管在函数执行时就已经存在.但该变量的作用域仍为块范围。存储时间不影响标识符的作用域。
只有函数原型参数表中使用的标识符才具有函数原型范围。前面曾介绍过,函数原型不要求参数表中使用的标识符名称,只要求类型。如果函数原型参数表中使用名称,则编译器忽略这些名称。
函数原型中使用的标识符可以在程序中的其他地方复用,不会产生歧义。
常见编程错误3.20
如果在内层块和外层块中使用同名标识符,而程序员又希望在内层块中引用外层块中的标识符,这通常会产生逻辑错误,因为实际上使用的还是内层块中标识符的值。
编程技巧3.10
避免隐藏外层块范围名称的变量名,因此要在程序中避免重复使用标识符。
图3.12的程序演示了全局变量、自动局部变量和static局部变量的作用域问题。
1 // Fig. 3.12:fig03 12.cpp
2 // A scoping example
3 #include <iostream.h>
4
5 void a( void ); // function prototype
6 void b( void ); // function prototype
7 void c void ); // function prototype
8
9 int x = 1; // global variable
10
11 int main( )
12 {
13 int x - 5; // local variable to main
14
15 cout << "local x in outer scope of main is "<< x << endl;
16
17 { // start new scope
18 int x = 7;
19
20 cout << "local x in inner scope of main is "<< x << endl;
21 } // end new scope
22
23 cout << "local x in outer scope of main is" << x << endl;
24
25 a( ); // a has automatic local x
26 b( ); // b has static local x
27 c( ); // c uses global x
28 a( ); // a reinitializes automatic local x
29 b( ); // static local x retains its previous value
30 c( ); // global x also retains its value
31
32 cout << "local x in main is "<< x << endl;
33
34 return 0;
35 }
36
37 void a( void )
38 {
39 int x=25; // initiallzed each time a is called
40
41 cout << endl << "local x in a is "<< x
42 <<" after entering a" << endl;
43 ++x;
44 cout << "local x in a is "<< x
45 << "before exiting a" << endl;
46 }
47
48 void b( void )
49
50 static int x = 50; // Static initialization only
51 // first time b is called.
52 cout << endl << "local static x is "<< x
53 << - on entering b" << endl;
54 ++x;
55 cout << "local static x is" << x
56 << "on exiting b" << endl;
57 }
58
59 void c( void )
6O
61 cout << endl << "global x is "<< x
62 << "on entering c" << endl;
63 x *= 10;
64 cout << "global x is "<< x << "on exiting c" << endl;
65 }
输出结果:
local x in outer scope Of main is 5
local x in inner scope Of main iS 7
local x in outer scope Of main is 5
local x in a is 25 after entering a
local x in a ls 26 before exiting a
local static x is 50 On entering b
local static x is 51 On exiting b
global x is 1 on entering c
global x is 10 on exiting c
local x in a is 25 after entering a
local x in a is 26 before exiting a
local static x is 51 on entering b
local statzc x is 52 On exiting b
global x is lO On entering c
global x is 100 On exiting c
local x in main is 5
图3.12说明变量作用域的例子
全局变量x声明并初始化为1。这个全局变量在任何声明x变量的块和函数中隐藏。在main函数中,局部变量x声明并初始化为5。打印这个变量,结果表示全局变量x在main函数中隐藏。然后在main函数中定义一个新块,将另一个局部变量x声明并初始化为7,打印这个变量,结果表示其隐藏main函数外层块中的x。数值为7的变量x在退出这个块时自动删除,并打印main函数外层块中的局部变量x,表示其不再隐藏。程序定义三个函数,都设有参数和返回值。函数a定义自动变量x并将其初始化为25。调用a时,打印该变量,递增其值,并在退出函数之前再次打印该值。每次调用该函数时,自动变量x重新初值化为25。函数b声明static变量x并将其初始化为10。声明为static的局部变量在离开作用域时仍然保持其数值。调用b时,打印x,递增其值,并在退出函数之前再次打印该值。下次调用这个函数时,static局部变量x包含数值51。函数c不声明任何变量,因此,函数c引用变量x时,使用全局变量x。调用函数c时,打印全局变量,将其乘以10,并在退出函数之前再次打印该值。下次调用函数c时,全局变量已变为10。最后,程序再次打印main函数中的局部变量x,结果表示所有函数调用都没有修改x的值,因为函数引用的都是其他范围中的变量。
3.12 递归
前面介绍的程序通常由严格按层次方式调用的函数组成。对有些问题,可以用自己调用自己的函数。递归函数(recursive function)是直接调用自己或通过另一函数间接调用自己的函数。递归是个重要问题,在高级计算机科学教程中都会详细介绍。本节和下节介绍一些简单递归例子,本书则包含大量递归处理。图3.17(3.14节末尾)总结了本书的递归例子和练习。
我们先介绍递归概念,然后再介绍几个包含递归函数的程序。递归问题的解决方法有许多相同之处。调用递归函数解决问题时,函数实际上只知道如何解决最简单的情况(称为基本情况)。对基本情况的函数调用只是简单地返回一个结果。如果在更复杂的问题中调用函数,则函数将问题分成两个概念性部分:函数中能够处理的部分和函数中不能够处理的部分。为了进行递归,后者要模拟原问题,但稍作简化或缩小。由于这个新问题与原问题相似,因此函数启动(调用)自己的最新副本来处理这个较小的问题,称为递归调用(reeursive call)或递归步骤(reeursive step)。递归步聚还包括关键字return,因为其结果与函数中需要处理的部分组合,形成的结果返回原调用者(可能是main)。递归步骤在原函数调用仍然打开时执行,即原调用还没有完成。
递归步骤可能导致更多递归调用,因为函数-继续把函数调用的新的子问题分解为两个概念性部分。要让递归最终停止,每次函数调用时都使问题进一步简化,从而产生越来越小的问题.最终合并到基本情况。这时,函数能识别并处理这个基本情况,并向前一个函数副本返回结果,并回溯一系列结果,直到原函数调用最终把最后结果返回给main。这一切比起前面介绍的其他问题似乎相当复杂.下面通过一个例子来说明。我们用递归程序进行一个著名的数学计算。
非负整数n的阶乘写成n!,为下列数的积:
n*(n—1)·(n—2)*...*l
其中1,等于1, 0定义为1。例如,51为5*4*3*2*1.即120。
整数number大于或等于0时的阶乘可以用下列for循环迭代(非递归)计算:
factorial = l;
for { int counter = number;counter >= 1;counter-- )
factorial *= counter;
通过下列关系可以得到阶乘函数的递归定义:
n!=n*(n-1)!
例如,5!等于5*4!,如下所示:
5!=5*4*3*2*l
5!=5*(4*3*2*1)
5!=5*(4!)
求值5!的过程如图3.13。图3.13 a)显示如何递归调用,直到1!求值为1,递归终止。图3.13 b)显示每次递归调用向调用者返回的值,直到计算和返回最后值。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -