程序结构

Recursion is the root of computation since it trades description for time. [^1]

目录


[TOC]

程序结构


零 前言

《C 语言程序设计——现代方法》一书中,将此章节安排在了函数之后。在我看来,这样安排不是很合理。在函数一章中,在解释形参和实参可以同名时,没有这部分知识确实很难去阐述原理。

书中本章开篇第一句话就是“本章来讨论一个程序有多个函数时所产生的几个问题”,其实变量的作用域生存期并不是在只有函数的时候才会应用到,举一个很简单的例子:for 循环。 其实这就是我们前面说到的(block)的问题。

学习过程中,【C 必知必会】系列中的【CMOOC】篇中的相关文章大家可以参考一下。

本节的内容不局限于 作用域 和 生存期。废话不多说,我们开始吧。

一 局部变量

函数体内声明的变量称为该函数的局部变量

比如:

int main(void){

int i;

return 0;
}

变量 i 就是局部变量。

局部变量的性质:
  • 自动存储期限。变量的存储期限(生存期)(storage duration)(或存储长度)。局部变量的存储单元在函数被调用时“自动”分配,函数返回时自动回收,所以称这种变量具有自动的存储期限。包含局部变量的函数返回时,局部变量的值无法保留。当再次调用该函数时,无法保证变量仍拥有原先的值。
  • 块作用域。变量的作用域是可以引用该变量的程序文本部分。局部变量拥有块作用域:从变量声明的点开始一直到所在函数体的末尾。因为变量的作用域不能延伸到其所属的函数之外,所以其他函数可以把同名变量用于其他用途。

这一段介绍写的太书面化了。其实上面说的无非就是生存期和作用域问题。

关于生存期和作用域的程序演示

下面的程序计算数组元素的和:

#include<stdio.h>

void sum_array(int a[], int len) {

int sum = 0;

for (int i = 0; i < len; i++) {
sum += a[i];
}

printf("sum is %d, length of array is %d", sum, len);
}

int main(void) {

int len = 10;
int a[5];

for (int i = 0; i < 5; i++) {
scanf("%d", &a[i]);
}

sum_array(a, len / 2);

return 0;
}
// 我们输入:1 2 3 4 5
// 输出: sum is 15, length of array is 5

我们将上面的程序改写为:

#include<stdio.h>

void sum_array(int a[], int i) {

int len = i;
int sum = 0;

for (int i = 0; i < len; i++) {
sum += a[i];
if (i == len - 1) {
i += 5;
}
}

printf("sum is %d, length of array is %d", sum, i);
}

int main(void) {

int i = 10;
int a[5];

for (int i = 0; i < 5; i++) {
scanf("%d", &a[i]);
}

sum_array(a, i / 2);

return 0;
}
// 我们输入:1 2 3 4 5
// 输出: sum is 15, length of array is 5

用简单的描述一下作用域和生存期:

作用域:限定某个名字的可用性的代码范围就是该名字的作用域

生存期:变量值存在的时间

块:一个花括号{}就是一个块。

通常来说,变量的作用域和生存期都是在一个块内。

上面第二个程序的执行结果和第一个完全一样,我们现在来一步一步分析一下:

// 新的块(函数)中,i 是一个新的变量,mian 函数中的 i 在这里不再生效(作用域和生存期失效)
// 这个 i 就是实参的值,也就是 5
void sum_array(int a[], int i) {

int len = i;
int sum = 0;

// for 语句内 i 的情况和 mian 函数中的一样
for (int i = 0; i < len; i++) {
sum += a[i];
// 为了证明 for 语句内的 i 和外面形参 i 完全不同,在即将退出循环时,我将 i 增加了 5,
// 所以退出循环时里面的 i 的值为 10
if (i == len - 1) {
i += 5;
}
}
// 最后输出的 i 依然是形参 5
printf("sum is %d, length of array is %d", sum, i);
}

int main(void) {

int i = 10;
int a[5];

//在 for 语句这个块内重新声明的 i ,这个 i 和上面的 i 是完全不同的变量。
// 修改 for 语句内的 i 不会影响外面的 i ,虽然外面的 i 在 for 语句内依然生效,但是可以理解为里面的 i 将其覆盖了
// 正所谓谁的地盘谁做主
for (int i = 0; i < 5; i++) {
scanf("%d", &a[i]);
}
// for 语句执行结束后,里面的 i 被自动回收了。i 不再生效。
// 所以下面的 i 就是外部的 i,也就是 10

sum_array(a, i / 2);

return 0;
}

C99 不要求函数在一开始的位置进行变量声明。所以局部变量的作用域可能会很小

1. 静态局部变量

在局部变量中放置单词 static 可以使变量具有静态存储期限而不再是自动存储期限。

因为具有静态存储期限的变量拥有永久的存储单元,所以在整个程序的执行期间都会保留变量的值。比如:

void func(){
int static n; // static locol variable
}

在函数 func 返回时,变量 n 的值不会丢失。

静态局部变量虽然生存期是整个程序,但是作用域尽在其所定义的块内。也就是说,上例中函数 func 返回后,func 内的 n 就不再可用。

#include<stdio.h>

void func() {

int static n = 0;

printf("%d\n", ++n);

}

int main(void) {

func();
func();
func();

return 0;
}
//输出:
1
2
3

二 全局变量

全局变量(外部变量 external variable)声明在所有函数体之外。

全局变量的性质
  • 静态存储期限

    #include<stdio.h>

    int i = 0;

    void func() {

    printf("%d\n", ++i);
    }

    int main(void) {

    func();
    func();
    func();

    return 0;
    }
    //输出:
    1
    2
    3
  • 文件作用域。全局变量的作用域:从变量被声明的点开始一直到所在文件的末尾。外部变量声明之后的函数都可以访问(并修改)它。

    #include<stdio.h>

    int i = 0;

    void func() {

    printf("%d\n", ++i);
    }

    void func1() {

    printf("%d\n", ++i);
    }

    int main(void) {

    func();
    func1();
    func();

    return 0;
    }
    //输出:
    1
    2
    3

程序:全局变量实现栈

是一种只能从一端“进出”的数据结构。这一端被称为栈顶。

我们可以用数组来模拟这种数据结构。用一个变量 top 标记当前栈顶的位置。如果数据压栈,则将 top 自增;如果数据出栈,则将 top 自减。我们需要写很多函数来实现这种数据结构,比如 压栈,出栈,判满,判空等等。我们可以将 表示栈顶的变量 top 和 表示栈的数组定义为全局变量。这里有一段代码(不是完整的程序):

#include<stdbool.h> // C99 only

#define STACK_SIZE 100
int stack[STACK_SIZE];
int top = -1;

void make_empty(){
top = -1;
}

bool is_empty(){
return top == -1;
}

bool is_full(){
return top == STACK_SIZE - 1;
}
//压栈
void push(int i){
if(is_full()){
printf("栈满!增加数据失败!");
return;
}
else{
stack[++top] = i;
}
}
//出栈
int pop(){
if(is_empty()){
printf("栈空!出栈失败!");
return;
}
else{
return stack[top--];
}
}

全局变量的利与弊

**利:**多个函数共享一个变量时或者少数几个函数共享大量变量时,外部变量很有用。

然而在大多数情况下,对于函数而言,传参比共享变量更好。原因如下:

弊:

  • 在程序维护期间,如果改变全局变量(比方说改变其类型),那么将需要检查同一文件中的每个函数,以确认该变化如何对函数产生影响。
  • 如果全局变量被赋了错误的值,可能很难确定出错的函数。
  • 很难在其他程序中复用依赖于全局变量的函数。依赖全局变量的函数不是“独立”的。为了在另一个程序中使用该函数,必须带上此函数需要的全局变量。
  • 如果全局变量在多个函数中使用(比如 for 循环的控制变量 i),让人误认为变量的使用彼此关联,而实际可能并非如此。

**注意:**使用全局变量时,要确保它们的名字都有意义。如果你发现全局变量的名字就像 itemp一样,这可能意味着这些变量其实应该是局部变量。

将局部变量声明为全局变量可能会导致一些问题。思考下例:

int i;

void print_one_row(void){
for(i = 1; i <= 10; i++)
printf("*");
}

void print_all_row(void){
for(i = 1; i <= 10; i++){
print_one_row();
printf("\n");
}
}

此时,print_all_row 打印的不是 10 行,而是 1 行。第一次调用 print_one_row 函数返回时, i 的值将为 11 ,不满足 for 的控制表达式,循环退出。

所以,全局变量建议不要使用。

程序:猜数

程序产生一个 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
You won in 3 guesses!

Play again?(Y/N) n

程序示例:

使用全局变量

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

#define MAX_NUMBER 100

int secret_number;// 要猜的数

void generate_secret_number();// 随机数生成
void read_guesses(); // 猜的实现

int main(void) {

char command;

printf("Guess the secret number between 1 and 100.\n");

do {
generate_secret_number();
printf("A new number has been chosen.\n");
read_guesses();
printf("Play again?(Y/N)");
scanf(" %c", &command);// 注意 %c 前的空格,这很重要
printf("\n");
} while (command == 'y' || command == 'Y');

return 0;
}

// 可以用这样的注释将函数的功能写在函数的定义的上方
// 我个人比较喜欢将简单的注释写在函数原型处

/****************************************************************************
*
* generate_secret_number: Initilizes the random number generator using the
* time of day.Randomly selects a number between
* 1 and MAX_NUMBER and stores it in secret_number
*
*****************************************************************************/

void generate_secret_number() {

srand((unsigned)time(NULL));

secret_number = rand() % MAX_NUMBER + 1;
}

/*****************************************************************
*
* read_guesses:Repeatedly reads user guesses and gives hints
* When guess is right,prints the total number of
* guesses and returns
*
******************************************************************/

void read_guesses() {

int guess, count = 0;

for (;;) {
printf("Enter guess: ");
scanf("%d", &guess);
count++;
if (guess > secret_number) {
printf("Too high; try again\n");
}
else if (guess < secret_number) {
printf("Too low; try again.\n");
}
else {
printf("You won in %d guesses!\n\n", count);
return;
}
}

不用全局变量

不用全局变量我们就需要让产生随机数的函数返回产生的随机数,然后将随机数当作参数传给 read_guesses() 函数。

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

#define MAX_NUMBER 100

int generate_secret_number();// 随机数生成
void read_guesses(int secret_number); // 猜的实现

int main(void) {

char command;
int secret_number;

printf("Guess the secret number between 1 and 100.\n");

do {
secret_number = generate_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;
}


int generate_secret_number() {

srand((unsigned)time(NULL));

int secret_number = rand() % MAX_NUMBER + 1;

return secret_number;
}

void read_guesses(int secret_number) {

int guess, count = 0;

for (;;) {
printf("Enter guess: ");
scanf("%d", &guess);
count++;
if (guess > secret_number) {
printf("Too high; try again\n");
}
else if (guess < secret_number) {
printf("Too low; try again.\n");
}
else {
printf("You won in %d guesses!\n\n", count);
return;
}
}
}

三 构建 C 程序

从 猜数 的程序中你应该大体可以感受到如何从头到尾去写一个 c 程序。我们这里给出比较好的编排顺序:

  1. #include指令
  2. #define指令
  3. 类型定义
  4. 全局变量声明
  5. 函数原型
  6. main 函数定义
  7. 其他函数定义

多写写程序自然会领略到其中的道理。

程序:手牌分类

编写程序对手牌进行读取和分类。手中的每张牌都有花色(方块,梅花,红桃和黑桃)和等级(2,3,4,5,6,7,8,9,T,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
Duplicated 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

程序示例:

/*
* 程序难点思路:
* 1)为了判定手中的牌是否重复,我们需要一个布尔类型数组存储整副牌,初始化整个数组为 false。如果一张牌已经在我们手上,那么我们将数组对应的元素置为 true
* 2)用两个分别数组来存储每个点数和花色的个数,这样方便我们后面判断牌的类型
* 3)8 种牌的类型,我们可拆成 同花,顺子,4张,3张,对子(值为 0,1,2)这五种基础类型的组合。
*
* 程序结构:
* 通过上面的分析,我们发现:这个程序需要 3 个数组和 5 个变量,如果都作为函数参数传参,显得有些笨。
* 而且,前面我们说过,函数只能返回一个值,那如果要将函数分离, 5 种基础类型就得放进数组;或者使用指针,而指针我们没有学习,而且指针还是逃不开传参
* 这样一分析,貌似使用全局变量是最好的做法了。对于初学者来说,这样可能确实是最好的。
* 但是,使用大量的全局变量是很不好的习惯,我不能让自己去写这样的代码。我认为:宁可这道题不做,也不能有坏的代码风格去写!
* 后面我们会学习自定义类型:结构体,它可能是这种问题最好的解决方法。
*
* 下面是这个问题的 4 种解决方法:
* 1)应用全局变量
* 2)应用指针作为函数参数
* 3)将判断卡牌类型的函数与打印函数合并
* 4)使用结构体
*
* 在这里,我坚持使用结构体来解决这类问题。全局变量大家只要知道概念即可,对于这道题来说,比起方法,可能设计程序的模块化思路更值得学习。
* 即使使用结构体,程序的主要逻辑也不会变。如果你非要用全局变量写,那你可以改写一下。
*/

#include<stdio.h>
#include<stdbool.h>

#define RANK 13
#define SUIT 4
#define CARD 5

typedef struct CardType {
bool flush; //同花
bool straight; //顺子
bool four; //四张
bool three; //三张
int pair; // 对子
// 0 表示不是 1 表示 1个对子 2 表示两个对子
bool cardInHand[SUIT][RANK]; // 判断此牌是否已在手中
int numRank[RANK]; // 每个点数的个数
int numSuit[SUIT]; // 每个花色的个数
}CardType;


void initCardType(CardType* card); // 初始化
void readCard(CardType* card); // 读取输入
void analyseCard(CardType* card); // 分析手牌
void printResult(CardType* card); //打印结果

int main(void) {

CardType card;

for (; ;) {
initCardType(&card);
readCard(&card);
analyseCard(&card);
printResult(&card);
}

return 0;
}


void initCardType(CardType* card) {

card->flush = false;
card->straight = false;
card->four = false;
card->three = false;
card->pair = 0;

int i, j;

for (i = 0; i < SUIT; i++) {
card->numSuit[i] = 0;
for (j = 0; j < RANK; j++) {
card->cardInHand[i][j] = false;
}
}

for (i = 0; i < RANK; i++) {
card->numRank[i] = 0;
}

}


void readCard(CardType* card) {

int card_read = CARD, rank, suit;
bool bad_card;
char ch;

while (card_read) {

bad_card = false; // 不要忘记重置坏牌的标记

printf("Enter a card : ");

// 判断点数
ch = getchar();
switch (ch) {
case '0': exit(0); break;
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; break;
}

ch = getchar();
switch (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; break;
}

// 检测输入是否多于两个字符
while ((ch = getchar()) != '\n') {
if (ch != ' ')
bad_card = true;
}

if (bad_card)
printf("Bad card; ignored.\n");
else if (card->cardInHand[suit][rank])
printf("Duplicated card; ignored.\n");
else {
++card->numRank[rank];
++card->numSuit[suit];
card->cardInHand[suit][rank] = true;
card_read--;
}
}
}

void analyseCard(CardType* card) {

int i, count;

// 同花是五张牌相同花色
for (i = 0; i < SUIT; i++) {
if (card->numSuit[i] == 5)
card->flush = true;
}

// 顺子是五张连续的牌,中间不能隔断

i = 0;
// 找到数组种第一张存在的牌
while (card->numRank[i] == 0)
i++;
count = 0;
for (; i < RANK && card->numRank[i] != 0; i++) {
count++;
}
// 顺子必须是五张
if (count == CARD) {
card->straight = true;
return; // 顺子肯定不是对子
}

for (i = 0; i < RANK; i++) {
if (card->numRank[i] == 4)
card->four = true;
if (card->numRank[i] == 3)
card->three = true;
if (card->numRank[i] == 2)
++card->pair;
}

}

void printResult(CardType* card) {

if (card->flush && card->straight)
printf("Stright flush\n");
else if (card->four)
printf("Four of a kind\n");
else if (card->three && card->pair == 1)
printf("Full house\n");
else if (card->flush)
printf("flush\n");
else if (card->straight)
printf("straight\n");
else if (card->three)
printf("Three of a kind\n");
else if (card->pair == 2)
printf("Two pairs\n");
else if (card->pair == 1)
printf("pair\n");
else
printf("High card\n");

printf("\n\n");
}

参考资料:《C语言程序设计:现代方法》

[^1]: 递归是计算之母。她用描述换取时间。 Epigrams on Programming 编程警句