——Will Rogers也一定会说:“没有自由变量这种东西。”
第9章已经介绍过函数了,因此本章就来讨论一个程序包含多个函数时所产生的几个问题。本章的前两节讨论局部变量
(10.1节)和外部变量
(10.2节)之间的差异,10.3节考虑程序块(含有声明的复合语句)
问题,10.4节解决用于局部名
、外部名
和在程序块中声明的名字
的作用域规则问题,10.5节介绍用来组织函数原型
、函数定义
、变量声明
和程序其他部分的方法。
我们把在函数体内声明的变量称为该函数的局部变量
。在下面的函数中,sum
是局部变量:
int sum_digits(int n)
{
int sum = 0; /* local variable */
while (n > 0) {
sum += n % 10;
n /= 10;
}
return sum;
}
默认情况下,局部变量
具有下列性质:
自动存储期
。变量的存储期(storage duration
,也称为延续)是程序执行时,能够确保变量的存储空间必定存在的那一部分时间。通常来说,局部变量的存储空间是在包含该变量的函数被调用时“自动”分配的,函数返回时收回分配,所以称这种变量具有自动存储期。包含局部变量的函数返回时,局部变量的值无法保留。当再次调用该函数时,无法保证变量仍拥有原先的值。块作用域
。变量的作用域是可以引用该变量的那一部分程序文本。局部变量拥有块作用域:从变量声明的点开始一直到所在函数体的末尾。因为局部变量的作用域不能延伸到其所属函数之外,所以其他函数可以把同名变量用于别的用途。
C99
不要求在函数一开始就进行变量声明,所以局部变量的作用域可能非常小。
在局部变量声明中放置单词static
可以使变量具有静态存储期而不再是自动存储期。因为具有静态存储期的变量拥有永久的存储单元,所以在整个程序执行期间都会保留变量的值。思考下面的函数:
void f(void)
{
static int i; /* static local variable */
...
}
因为局部变量
i
已经声明为static
,所以在程序执行期间它所占据的内存单元是不变的。在f
返回时,变量i
不会丢失其值。静态局部变量始终有块作用域,所以它对其他函数是不可见的。概括来说,静态变量是对其他函数隐藏数据的地方,但是它会为将来同一个函数的再调用保留这些数据。
形式参数拥有和局部变量一样的性质,即自动存储期和块作用域。事实上,形式参数和局部变量唯一真正的区别是,在每次函数调用时对形式参数自动进行初始化(调用中通过赋值获得相应实际参数的值)
。
传递参数是给函数传送信息的一种方法。函数还可以通过外部变量(external variable)
进行通信。外部变量是声明在任何函数体外的。
外部变量(有时称为全局变量
)的性质不同于局部变量的性质。
静态存储期
。就如同声明为static
的局部变量一样,外部变量拥有静态存储期。存储在外部变量中的值将永久保留下来。文件作用域
。外部变量拥有文件作用域:从变量被声明的点开始一直到所在文件的末尾。因此,跟随在外部变量声明之后的所有函数都可以访问(并修改)它。为了说明外部变量的使用方法,一起来看看称为
栈(stack)
的数据结构。(栈是抽象的概念,它不是C
语言的特性。大多数编程语言都可以实现栈。)像数组一样,栈可以存储具有相同数据类型的多个数据项。然而,栈操作是受限制的
:只可以往栈中压入数据项(把数据项加在一端——“栈顶”)或者从栈中弹出数据项(从同一端移走数据项)。禁止测试或修改不在栈顶的数据项
。
C
语言中实现栈的一种方法是把元素存储在数组中,我们称这个数组为contents
。命名为top
的一个整型变量用来标记栈顶的位置。栈为空时,top
的值为0
。为了往栈中压入数据项,可以把数据项简单存储在contents
中由top
指定的位置上,然后自增top
。弹出数据项则要求自减top
,然后用它作为contents
的索引取回弹出的数据项。基于上述这些概要,这里有一段代码(不是完整的程序)为栈声明了变量
contents
和top
并且提供了一组函数来表示对栈的操作。全部5
个函数都需要访问变量top
,而且其中2
个函数还都需要访问contents
,所以接下来把contents
和top
设为外部变量。
#include /* C99 only */
#define STACK_SIZE 100
/* external variables */
int contents[STACK_SIZE];
int top = 0;
void make_empty(void)
{
top = 0;
}
bool is_empty(void)
{
return top == 0;
}
bool is_full(void)
{
return top == STACK_SIZE;
}
void push(int i)
{
if(is_full())
stack_overflow();
else
contents[top++] = i;
}
int pop(void)
{
if(is_empty())
stack_underflow();
else
return contents[--top];
}
在多个函数必须共享一个变量时或者少数几个函数共享大量变量时,外部变量是很有用的。
然而在大多数情况下,对函数而言,通过形式参数进行通信比通过共享变量的方法更好
,原因列举如下:
许多
C
程序员过于依赖外部变量。一个普遍的陋习是,在不同的函数中为不同的目的使用同一个外部变量。假设几个函数都需要变量i
来控制for
语句。一些程序员不是在使用变量i
的每个函数中都声明它,而是在程序的顶部声明它,从而使得该变量对所有函数都是可见的。这种方式除了前面提到的几个缺点外,还会产生误导:以后阅读程序的人可能认为变量的使用彼此关联,而实际并非如此。
使用外部变量时,要确保它们都拥有有意义的名字
。(局部变量不是总需要有意义的名字的,因为往往很难为for
循环中的控制变量起一个比i
更好的名字。)如果你发现为外部变量使用的名字就像i
和temp
一样,这可能意味着这些变量其实应该是局部变量。
请注意!把原本应该是局部变量的变量声明为外部变量可能导致一些令人厌烦的错误,请看下面这个例子:
//我们希望显示一个由星号组成的10×10的图形:
int i;
void print_one_row(void)
{
for (i = 1; i <= 10; i++)
printf("*");
}
void print_all_rows(void)
{
for (i = 1; i <= 10; i++) {
print_one_row();
printf("\n");
}
}
/*
print_all_rows函数不是显示10行星号,而是只显示1行。
在第一次调用print_one_row函数后返回时,i的值将为11。
然后,print_all_rows函数中的for语句对变量i进行自增并判定它是否小于或等于10。
因为判定条件不满足,所以循环终止,函数返回。
*/
为了获得更多关于外部变量的经验,现在编写一个简单的游戏程序。这个程序产生一个1~100
的随机数,用户尝试用尽可能少的次数猜出这个数。
运行效果如下所示:
Guess the secret number between 1 and 100.
A new number has been chosen.
Enter guess: 55
Too low; try again.
Enter guess: 65
Too high; try again.
Enter guess: 60
Too high; try again.
Enter guess: 58
You won in 4 guesses!
Play again? (Y/N) y
A new number has been chosen.
Enter guess: 78
Too high; try again.
Enter guess: 34
You won in 2 guesses!
Play again? (Y/N) n
这个程序需要完成几个任务:初始化随机数生成器,选择神秘数,以及与用户交互直到选出正确数为止。如果编写独立的函数来处理每个任务,那么可能会得到下面的程序。
/*
guess.c(外部变量版本)
--Asks user to guess a hidden number
*/
#include
#include
#include
#define MAX_NUMBER 100
/* external variable */
int secret_number;
/* prototypes */
void initialize_number_generator(void);
void choose_new_secret_number(void);
void read_guesses(void);
int main(void)
{
char command;
printf("Guess the secret number between 1 and %d.\n\n", MAX_NUMBER);
initialize_number_generator();
do {
choose_new_secret_number();
printf("A new number has been chosen.\n");
read_guesses();
printf("Play again? (Y/N) ");
scanf(" %c", &command);
printf("\n");
} while (command == 'y' || command == 'Y');
return 0;
}
/************************************************************
* initialize_number_generator: Initializes the random *
* number generator using *
* the time of day. *
************************************************************/
void initialize_number_generator(void)
{
srand((unsigned)time(NULL));
}
/************************************************************
* choose_new_secret_number: Randomly selects a number *
* between 1 and MAX_NUMBER and *
* stores it in secret_number. *
************************************************************/
void choose_new_secret_number(void)
{
secret_number = rand() % MAX_NUMBER + 1;
}
/************************************************************
* read_guesses: Repeatedly reads user guesses and tells *
* the user whether each guess is too low, *
* too high, or correct. When the guess is *
* correct, prints the total number of *
* guesses and returns. *
************************************************************/
void read_guesses(void)
{
int guess, num_guesses = 0;
for (;;) {
num_guesses++;
printf("Enter guess: ");
scanf("%d", &guess);
if(guess == secret_number) {
printf("You won in %d guesses!\n\n", num_guesses);
return;
}else if(guess < secret_number)
printf("Too low; try again.\n");
else
printf("Too high; try again.\n");
}
}
对于随机数的生成,这次将缩放rand
函数的返回值使其落在1~MAX_NUMBER
范围内。虽然guess.c
程序工作正常,但是它依赖一个外部变量。把变量secret_number
外部化以便choose_new_secret_number
函数和read_guesses
函数都可以访问它。
如果对
choose_new_secret_number
函数和read_guesses
函数稍做改动,应该能把变量secret_number
移入main
函数中。现在我们将修改choose_new_secret_number
函数以便函数返回新值,并将重写read_guesses
函数以便变量secret_number
可以作为参数传递给它,具体请看下面这个程序。
/*
guess2.c(形式参数版本)
--Asks user to guess a hidden number
*/
#include
#include
#include
#define MAX_NUMBER 100
/* prototypes */
void initialize_number_generator(void);
int new_secret_number(void); //修改
void read_guesses(int secret_number); //修改
int main(void)
{
char command;
int secret_number; //修改
printf("Guess the secret number between 1 and %d.\n\n", MAX_NUMBER);
initialize_number_generator();
do {
secret_number = new_secret_number(); //修改
printf("A new number has been chosen.\n");
read_guesses(secret_number); //修改
printf("Play again? (Y/N) ");
scanf(" %c", &command);
printf("\n");
} while (command == 'y' || command == 'Y');
return 0;
}
/************************************************************
* initialize_number_generator: Initializes the random *
* number generator using *
* the time of day. *
************************************************************/
void initialize_number_generator(void)
{
srand((unsigned)time(NULL));
}
/************************************************************
* new_secret_number: Returns a randomly chosen number *
* between 1 and MAX_NUMBER. *
************************************************************/
int new_secret_number(void) //修改
{
return rand() % MAX_NUMBER + 1;
}
/************************************************************
* read_guesses: Repeatedly reads user guesses and tells *
* the user whether each guess is too low, *
* too high, or correct. When the guess is *
* correct, prints the total number of *
* guesses and returns. *
************************************************************/
void read_guesses(int secret_number) //修改
{
int guess, num_guesses = 0;
for (;;) {
num_guesses++;
printf("Enter guess: ");
scanf("%d", &guess);
if (guess == secret_number) {
printf("You won in %d guesses!\n\n", num_guesses);
return;
}else if(guess < secret_number)
printf("Too low; try again.\n");
else
printf("Too high; try again.\n");
}
}
5.2
节遇到过复合语句,一个复合语句也是一个块(block)
,但块并非只有复合语句这一种形式。块也叫程序块
。下面是程序块的示例:
if (i > j) {
/* swap values of i and j */
int temp = i;
i = j;
j = temp;
}
这里,整个
if
语句是一个程序块;if
语句的每一个子句也是程序块。默认情况下,声明在程序块中的变量的存储期是自动的:进入程序块时为变量分配存储单元,退出程序块时收回分配的空间。变量具有块作用域,也就是说,不能在程序块外引用
。
函数体是程序块
。在需要临时使用变量时,函数体内的程序块也是非常有用的。在上面这个例子中,我们需要一个临时变量以便可以交换i
和j
的值。在程序块中放置临时变量有两个好处:(1)避免函数体起始位置的声明与只是临时使用的变量相混淆;(2)减少了名字冲突。在此例中,名字temp
可以根据不同的目的用于同一函数中的其他地方,在程序块中声明的变量temp
严格属于局部程序块。
C99
允许在程序块的任何地方声明变量,就像允许在函数体内的任何地方声明变量一样。
在C
程序中,相同的标识符可以有不同的含义。C
语言的作用域规则使得程序员(和编译器)可以确定与程序中给定点相关的是哪种含义。
下面是最重要的作用域规则:当程序块内的声明命名一个标识符时,如果此标识符已经是可见的(因为此标识符拥有文件作用域,或者因为它已在某个程序块内声明),新的声明临时“隐藏”了旧的声明,标识符获得了新的含义。在程序块的末尾,标识符重新获得旧的含义。
思考下面这个(有点极端的)例子,例子中的标识符i
有4
种不同的含义。
i
是具有静态存储期和文件作用域的变量。i
是具有块作用域的形式参数。i
是具有块作用域的自动变量。i
也是具有块作用域的自动变量。int i; //声明1
void f(int i){ //声明2
i = 1;
}
void g(void){
int i = 2; //声明3
if(i > 0){
int i; //声明4
i = 3;
}
i = 4;
}
void h(void){
i = 5;
}
一共使用了5
次i
。C
语言的作用域规则允许确定每种情况中i
的含义。
i = 1
引用了声明2中的形式参数,而不是声明1中的变量。i > 0
引用了声明3中的变量。i = 3
引用了声明4中的变量。i = 4
引用了声明3中的变量。声明4超出了作用域,所以不能引用。i = 5
引用了声明1中的变量。我们已经看过构成
C
程序的主要元素,现在应该为编排这些元素开发一套方法了。目前只考虑单个文件的程序,第15章
会说明如何组织多个文件的程序。
迄今为止,已经知道程序可以包含:
include
和#define
这样的预处理指令;
C
语言对上述这些项的顺序要求极少:
C
语言对函数没有什么要求,但是这里强烈建议在第一次调用函数前要对每个函数进行定义或声明。( 至少C99
要求我们这么做。)为了遵守这些规则,这里有几个构建程序的方法。下面是一种可能的编排顺序:
#include
指令;#define
指令;main
函数之外的函数的原型;main
函数的定义;因为
#include
指令带来的信息可能在程序中的好几个地方都需要,所以先放置这条指令是合理的。
#define
指令创建宏,对这些宏的使用通常遍布整个程序。类型定义放置在外部变量声明的上面是合乎逻辑的,因为这些外部变量的声明可能会引用刚刚定义的类型名。
接下来,声明外部变量使得它们对于跟随在其后的所有函数都是可用的。在编译器看见原型之前调用函数,可能会产生问题,而此时声明除了
main
函数以外的所有函数可以避免这些问题。这种方法也使得无论用什么顺序编排函数定义都是可能的。例如,根据函数名的字母顺序编排,或者把相关函数组合在一起进行编排。在其他函数前定义main
函数使得阅读程序的人容易定位程序的起始点。
最后的建议:在每个函数定义前放盒型注释
可以给出函数名、描述函数的目的、讨论每个式参数的含义、描述返回值(如果有的话)并罗列所有的副作用(如修改了外部变量的值)。
为了说明构建
C
程序的方法,下面编写一个比前面的例子更复杂的程序——给一手牌分类。这个程序会对一手牌进行读取和分类。手中的每张牌都有花色(方块、梅花、红桃和黑桃)和点数(2、3、4、5、6、7、8、9、10、J、Q、K和A
)。不允许使用王牌,并且假设A
是最高的点数。程序将读取一手5
张牌,然后把手中的牌分为下列某一类(列出的顺序从最好到最坏)。分类如下:
4
张牌点数相同)。3
张牌是同样的点数,另外2
张牌是同样的点数)。5
张牌是同花色的)。5
张牌的点数顺序相连)。3
张牌的点数相同)。2
张牌的点数相同)。如果一手牌可分为两种或多种类别,程序将选择最好的一种。
为了便于输入,把牌的点数和花色简化如下(字母可以是大写,也可以是小写)。
2 3 4 5 6 7 8 9 t j q k a
。c d h s
。如果用户输入非法牌或者输入同一张牌两次,程序将忽略此牌,产生出错消息,然后要求输入另外一张牌。如果输入为0
而不是一张牌,就会导致程序终止。
程序运行会话如下所示:
Enter a card: 2s
Enter a card: 5s
Enter a card: 4s
Enter a card: 3s
Enter a card: 6s
Straight flush
Enter a card: 8c
Enter a card: as
Enter a card: 8c
Duplicate card; ignored.
Enter a card: 7c
Enter a card: ad
Enter a card: 3h
Pair
Enter a card: 6s
Enter a card: d2
Bad card; ignored.
Enter a card: 2d
Enter a card: 9c
Enter a card: 4h
Enter a card: ts
High card
Enter a card: 0
从上述程序的描述可以看出它有3
个任务:
5
张牌;把程序分为
3
个函数,分别完成上述3
个任务,即read_cards
函数、analyze_hand
函数和print_result
函数。main
函数只负责在无限循环中调用这些函数。这些函数需要共享大量的信息,所以让它们通过外部变量来进行交流。read_cards
函数将与一手牌相关的信息存进几个外部变量中,然后analyze_hand
函数将检查这些外部变量,把结果分类放在便于print_result
函数显示的其他外部变量中。
/*
poker.c
--Classifies a poker hand
*/
#include /* C99 only */
#include
#include
#define NUM_RANKS 13
#define NUM_SUITS 4
#define NUM_CARDS 5
/* external variables */
int num_in_rank[NUM_RANKS];
int num_in_suit[NUM_SUITS];
bool straight, flush, four, three;
int pairs; /* can be 0, 1, or 2 */
/* prototypes */
void read_cards(void);
void analyze_hand(void);
void print_result(void);
/************************************************************
* main: Calls read_cards, analyze_hand, and print_result *
* repeatedly. *
************************************************************/
int main(void)
{
for (;;) {
read_cards();
analyze_hand();
print_result();
}
}
/************************************************************
* read_cards: Reads the cards into the external *
* variables num_in_rank and num_in_suit; *
* checks for bad cards and duplicate cards. *
************************************************************/
void read_cards(void)
{
bool card_exists[NUM_RANKS][NUM_SUITS];
char ch, rank_ch, suit_ch;
int rank, suit;
bool bad_card;
int cards_read = 0;
for (rank = 0; rank < NUM_RANKS; rank++) {
num_in_rank[rank] = 0;
for (suit = 0; suit < NUM_SUITS; suit++)
card_exists[rank][suit] = false;
}
for (suit = 0; suit < NUM_SUITS; suit++)
num_in_suit[suit] = 0;
while (cards_read < NUM_CARDS) {
bad_card = false;
printf("Enter a card: ");
rank_ch = getchar();
switch (rank_ch) {
case '0': exit(EXIT_SUCCESS);
case '2': rank = 0; break;
case '3': rank = 1; break;
case '4': rank = 2; break;
case '5': rank = 3; break;
case '6': rank = 4; break;
case '7': rank = 5; break;
case '8': rank = 6; break;
case '9': rank = 7; break;
case 't': case 'T': rank = 8; break;
case 'j': case 'J': rank = 9; break;
case 'q': case 'Q': rank = 10; break;
case 'k': case 'K': rank = 11; break;
case 'a': case 'A': rank = 12; break;
default: bad_card = true;
}
suit_ch = getchar();
switch (suit_ch) {
case 'c': case 'C': suit = 0; break;
case 'd': case 'D': suit = 1; break;
case 'h': case 'H': suit = 2; break;
case 's': case 'S': suit = 3; break;
default: bad_card = true;
}
while ((ch = getchar()) != '\n')
if (ch != ' ') bad_card = true;
if (bad_card)
printf("Bad card; ignored.\n");
else if (card_exists[rank][suit])
printf("Duplicate card; ignored.\n");
else {
num_in_rank[rank]++;
num_in_suit[suit]++;
card_exists[rank][suit] = true;
cards_read++;
}
}
}
/************************************************************
* analyze_hand: Determines whether the hand contains a *
* straight, a flush, four-of-a-kind, *
* and/or three-of-a-kind; determines the *
* number of pairs; stores the results into *
* the external variables straight, flush, *
* four, three, and pairs. *
************************************************************/
void analyze_hand(void)
{
int num_consec = 0;
int rank, suit;
straight = false;
flush = false;
four = false;
three = false;
pairs = 0;
/* check for flush */
for (suit = 0; suit < NUM_SUITS; suit++)
if (num_in_suit[suit] == NUM_CARDS)
flush = true;
/* check for straight */
rank = 0;
while (num_in_rank[rank] == 0) rank++;
for (; rank < NUM_RANKS && num_in_rank[rank] > 0; rank++)
num_consec++;
if (num_consec == NUM_CARDS) {
straight = true;
return;
}
/* check for 4-of-a-kind, 3-of-a-kind, and pairs */
for (rank = 0; rank < NUM_RANKS; rank++) {
if (num_in_rank[rank] == 4) four = true;
if (num_in_rank[rank] == 3) three = true;
if (num_in_rank[rank] == 2) pairs++;
}
}
/************************************************************
* print_result: prints the classification of the hand, *
* based on the values of the external *
* variables straight, flush, four, three, *
* and pairs. *
************************************************************/
void print_result(void)
{
if (straight && flush) printf("Straight flush");
else if (four) printf("Four of a kind");
else if (three &&
pairs == 1) printf("Full house");
else if (flush) printf("Flush");
else if (straight) printf("Straight");
else if (three) printf("Three of a kind");
else if (pairs == 2) printf("Two pairs");
else if (pairs == 1) printf("Pair");
else printf("High card");
printf("\n\n");
}
注意read_cards
函数中exit
函数的使用(第一个switch
语句的分支'0'
)。因为exit
函数具有在任何地方终止程序执行的能力,所以它对于此程序是十分方便的。
问1:具有静态存储期的局部变量会对递归函数产生什么影响?
答:当函数是递归函数时,每次调用它都会产生其自动变量的新副本。静态变量就不会发生这样的情况,相反,所有的函数调用都共享同一个静态变量。
问2:在下面的例子中,
j
初始化为和i
一样的值,但是有两个命名为i
的变量:
int i = 1;
void f(void)
{
int j = i;
int i = 2;
...
}
这段代码是否合法?如果合法,j
的初始值是1
还是2
?
答:代码是合法的。局部变量的作用域是从声明处开始的。因此,j
的声明引用了名为i
的外部变量。j
的初始值是1
。
本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!