1、第9章 模块化开发,自顶向下的分解 模块划分 库的设计与实现 库的应用,自顶向下的分解 模块划分 库的设计与实现 库的应用,模块化是指解决一个复杂问题时自顶向下逐层把软件系统划分成若干模块的过程,由此分解来降低复杂性。,自顶向下的分解,例子:猜硬币的游戏,功能: 提供游戏指南; 计算机随机产生正反面,让用户猜,报告对错结果。重复此过程,直到用户不想玩了为止。,自顶向下的分解,顶层分解,程序要做两件事:显示程序指南;模拟玩游戏的过程。,main( ) 显示游戏介绍;玩游戏; ,主程序的两个步骤是相互独立的,没有什么联系,因此可设计成两个独立的函数: void prn_instruction();
2、 void play();,自顶向下的分解,int main() prn_instruction();play();return 0; ,自顶向下的分解,prn_instruction的实现,prn_instruction函数的实现非常简单,只要一系列的输出语句把程序指南显示一下就可以了,void prn_instruction() cout “这是一个猜硬币正反面的游戏.n“;cout “我会扔一个硬币,你来猜.n“;cout “如果猜对了,你赢,否则我赢。n“; ,自顶向下的分解,play函数的实现,Play函数随机产生正反面,让用户猜,报告对错结果,然后询问是否要继续玩 即,计算机随机给
3、出一个结果,用户输入一个结果,两者比较,void play() char flag = y; /确定用户是否需要继续玩while (flag = Y | flag = y) coin = 生成正反面; /计算机随机产生正反面输入用户的猜测;if (用户猜测 = coin) 报告本次猜测结果正确;else 报告本次猜测结果错误;cinflag; ,自顶向下的分解,play函数的细化,生成正反面:如果用0表示正面,1表示反面,那么生成正反面就是随机生成0和1两个数 输入用户的猜测。如果不考虑程序的鲁棒性,这个问题直接用一个输入语句即可。但想让程序做得好一点,就必须考虑得全面一些。比如,用户可以不守
4、规则,既不输入0也不输入1,而是输入一个其他值,程序该怎么办?因此这个任务还可以进一步细化,所以再把它抽象成一个函数get_call_from_user。,自顶向下的分解,void play() int coin ;char flag = Y;srand(time(NULL); /设置随机数种子while (flag = Y | flag =y) coin = rand() * 2 / (RAND_MAX+1);/生成扔硬币的结果if (get_call_from_user() = coin) cout flag; ,自顶向下的分解,get_call_from_user的实现,该函数接收用户输
5、入的一个整型数。如果输入的数不是0或1,则重新输入,否则返回输入的值,int get_call_from_user() int guess; / 0 = head, 1 = taildo cout guess; while (guess !=0 ,自顶向下的分解,自顶向下的分解,main( ) 显示游戏介绍;玩游戏;,int main() prn_instruction();play();return 0; ,rand() int get_call_from_user(),问题的分解过程,自顶向下的分解 模块划分 库的设计与实现 库的应用,下面的三个文件同属于一个工程文件,指出下面程序的执行结
6、果,/cpp3.cpp #include int n; void func1( );void main( ) n=10; coutnendl; func1( );,/cpp1.cpp static int n; void func2( ); void func1( ) n+; func2( ); coutnendl;,/cpp2.cpp extern int n; void func2( ) n+; coutnendl;,10 11 1,模块划分,当程序变得更多更长更复杂的时候,要在一个单独的源文件中处理众多的函数会变得困难 把程序再分成几个小的源文件。每个源文件都包含一组相关的函数。一个源文
7、件被称为一个模块。模块划分标准:块内联系尽可能大,块间联系尽可能小,石头、剪刀、布游戏,游戏规则 布覆盖石头 石头砸坏剪刀 剪刀剪碎布 游戏的过程为:游戏者选择出石头、剪子或布;计算机随机选择一个;根据规则,输出结果;继续游戏,直到游戏者选择结束为止。在此过程中,游戏者可以随时阅读游戏指南或查看当前战况。,模块划分,第一层的分解,本程序需要实现的:根据用户输入的要求,实现不同的操作。分析一下,用户有可能有多少种可能的输入要求? 输入石头剪刀布 继续玩 输入阅读游戏指南 显示游戏指南 输入查看当前状况 显示游戏战况 输入结束游戏 不完了,最后显示游戏战况,模块划分,第一层的分解 的伪代码表示,W
8、hile (用户输入 != quit)switch(用户的选择) case paper, rock, scissor: 机器选择;评判结果;报告结果;case game: 显示目前的战况;case help: 显示帮助信息;default: 报告错误; 显示战况;,模块划分,函数抽取,获取用户输入selection_by_player 获取机器输入selection_by_machine 评判结果compare 报告结果并记录结果信息report 显示目前战况prn_game_status 显示帮助信息prn_help六个函数,模块划分,模块划分,分成四个模块: 主模块: main函数 获取选
9、择的模块: selection_by_player selection_by_machine 比较模块: compare 输出模块: report、prn_game_statusprn_help函数,枚举类型的定义,为了提高程序的可读性,我们定义两个枚举类型 :enum p_r_s paper, rock, scissor, game, help, quit ;enum outcome win, lose, tie, error ;,模块划分,枚举声明用于声明新的类型,能够允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示。 访问修辞符 enum 枚举名 枚举成员 ,例如上面定义
10、了两个新的类型 p_r_s和outcome。接着我们可以用这两个类型去定义变量,如: p_r_s aa; outcome ww;此时,aa与ww的允许取值范围为各自类型定义好的枚举成员,Select模块的设计,selection_by_player从键盘接收用户的输入并返回此输入值。因此,原型为p_r_s selection_by_player();selection_by_machine函数由机器产生一个石头、剪子、布的值,并返回。因此,原型为p_r_s selection_by_machine();,模块划分,Compare模块的设计,compare函数比较用户输入的值和机器产生的值,确定
11、输赢。 它要有两个参数,都是p_r_s类型的,它也应该有一个返回值,就是判断的结果 。 原型为:outcome compare(p_r_s, p_r_s);,模块划分,print模块的设计,prn_help显示一个用户输入的指南,告诉用户如何输入他的选择。因此,它没有参数也没有返回值。Report函数报告输赢结果,并记录输赢的次数。因此它必须有四个参数:输赢结果,更新输的次数、赢的次数和平局的次数,但没有返回值。prn_game_status函数报告至今为止的战况,因此需要三个参数:输的次数、赢的次数和平的次数,但没有返回值。,模块划分,print模块的进一步考虑,输的次数、赢的次数和平局的次
12、数在Report和prn_game_status两个函数中都出现。 Report函数修改这些变量的值,prn_game_status函数显示这些变量的值。 这三个函数的原型和用户期望的原型不一致,用户不希望原型中有参数。 输的次数、赢的次数和平局的次数和其他模块的函数无任何关系,因此可作为该模块的内部状态。内部状态可以作为该模块的全局变量 这样report和prn_game_status函数中都不需要这三个参数了。,模块划分,说明模块划分还是有道理的,这几个函数之间联系紧密 (需要使用共同的参数),三个打印函数的原型:void prn_help(); 没有参数也没有返回值void Report
13、(outcome); 1个参数,没有返回值void prn_game_status (); 没有参数也没有返回值,模块划分,模块划分,重新认识四个模块: 主模块: main函数 获取选择的模块: p_r_s selection_by_player();p_r_s selection_by_machine(); 比较模块: outcome compare(p_r_s, p_r_s); 输出模块: void prn_help(); void Report(outcome); void prn_game_status ();,main.cpp,select.cpp,compare.cpp,print
14、.cpp,怎样解决多模块的函数声明和预处理命令,/main.cpp p_r_s selection_by_player(); p_r_s selection_by_machine(); outcome compare(p_r_s, p_r_s); void prn_help(); void Report(outcome); void prn_game_status ();#include using namespace std;,/select.cpp p_r_s selection_by_player(); p_r_s selection_by_machine();#include #inc
15、lude #include using namespace std;,/compare.cppoutcome compare(p_r_s, p_r_s);#include using namespace std;,/print.cpp outcome compare(p_r_s, p_r_s); void prn_help(); void Report(outcome); void prn_game_status ();#include using namespace std;,头文件的设计,为方便起见,我们把所有的符号常量定义、类型定义和函数原型声明写在一个头文件中,让每个模块都includ
16、e这个头文件。那么,每个模块就不必要再写那些函数的原型声明了但这样做又会引起另一个问题,当把这些模块连接起来时,编译器会发现这些类型定义、符号常量和函数原型的声明在程序中反复出现多次 解决方法:需要用到一个新的编译预处理命令: #ifndef 标识符 #endif,模块划分,头文件的格式,#ifndef _name_h #define _name_h头文件真正需要写的内容 #endif,模块划分,石头、剪子、布游戏的头文件,/ 文件:p_r_s.h / 本文件定义了两个枚举类型,声明了本程序包括的所有函数原型#ifndef P_R_S #define P_R_S#include #includ
17、e #include using namespace std;enum p_r_s paper, rock, scissor, game, help, quit ;enum outcome win, lose, tie, error ;outcome compare(p_r_s player_choice, p_r_s machine_choice);void prn_game_status();void prn_help();void report(outcome result);p_r_s selection_by_machine();p_r_s selection_by_player()
18、; #endif,模块划分,主模块的实现,/文件:main.cpp / 石头、剪子、布游戏的主模块#include “p_r_s.h“int main() outcome result;p_r_s player_choice, machine_choice;/ seed the random number generator srand(time(NULL);,模块划分,while(player_choice = selection_by_player() != quit)switch(player_choice) case paper: case rock: case scissor:mac
19、hine_choice = selection_by_machine();result = compare(player_choice, machine_choice);report(result); break;case game:prn_game_status(); break;case help: prn_help(); break;default: cout “ PROGRAMMER ERROR!nn“; exit(1);prn_game_status(); return 0; ,模块划分,select模块的实现,/文件:select.cpp /包括机器选择selection_by_m
20、achine和 /玩家选择selection_by_player函数的实现#include “p_r_s.h“p_r_s selection_by_machine( ) int select = (rand( ) * 3 / (RAND_MAX + 1); cout “ I am “;switch(select)case 0: cout “paper. “; break;case 1: cout “rock. “; break;case 2: cout “scissor. “; break;return (p_r_s) select); ,模块划分,把int类型强制转换成枚举类型,p_r_s
21、selection_by_player() char c;p_r_s player_choice;prn_help(); /显示输入提示cout c;switch(c) case p: player_choice = paper; cout “you are paper. “; break;case r: player_choice = rock; cout “you are rock. “; break;case s: player_choice = scissor; cout “you are scissor. “;break;case g: player_choice = game; b
22、reak;case q: player_choice = quit; break;default : player_choice = help; break;return player_choice; ,模块划分,Compare模块的实现,/文件:compare.cpp /包括compare函数的实现#include “p_r_s.h“outcome compare(p_r_s player_choice, p_r_s machine_choice) outcome result;if (player_choice = machine_choice) return tie;switch(pla
23、yer_choice) case paper: result = (machine_choice = rock) ? win : lose; break; case rock: result = (machine_choice = scissor) ? win : lose; break;case scissor: result = (machine_choice = paper) ? win : lose; break;default: cout “ PROGRAMMER ERROR:Unexpected choice!nn“;exit(1); return result; ,模块划分,Pr
24、int模块的实现,/文件:print.cpp /包括所有与输出有关的模块。 /有prn_game_status,prn_help和report函数#include “p_r_s.h“int win_cnt = 0, lose_cnt = 0, tie_cnt = 0; /模块的内部状态,模块划分,void report(outcome result) switch(result) case win: +win_cnt; cout “You win. n“; break;case lose: +lose_cnt;cout “You lose.n“; break;case tie: +tie_cn
25、t;cout “A tie.n“; break;default: cout “ PROGRAMMER ERROR!nn“; exit(1); ,模块划分,void prn_game_status() cout endl ;cout “GAME STATUS:“ endl; cout “win: “ win_cnt endl;cout “Lose: “ lose_cnt endl;cout “tie: “ tie_cnt endl;cout “Total:“ win_cnt + lose_cnt + tie_cnt endl; ,模块划分,void prn_help() cout endl “T
26、he following characters can be used:n“ “ p for papern“ “ r for rockn“ “ s for scissorsn“ “ g print the game statusn“ “ h help, print this listn“ “ q quit the gamen“; ,模块划分,模块划分,文件:p_r_s.h 文件:main.cpp 文件:select.cpp 文件:compare.cpp 文件:print.cpp,问题分解,分模块实现,头文件,自顶向下的分解 模块划分 库的设计与实现 库的应用,设计自己的库,如果你的工作经常要用
27、到一些特殊的工具,你可以设计自己的库 一个库应该有一个主题。一个库中的函数都应该是处理同一类问题。如标准库iostream包含输入输出功能,cmath包含数学运算函数。我们自己设计的库也要有一个主题。 设计一个库还要考虑到它的通用性。库中的功能应来源于某一应用,但不局限于该应用,而且要高于该应用。在某一应用程序中提取库内容时应尽量考虑到兼容更多的应用,使其他应用程序也能共享这个库。,库的设计和实现,设计库的接口:(用户需要知道的部分) 库的用户必须了解的内容,包括库中函数的原型、这些函数用到的符号常量和自定义类型 接口表现为一个头文件 设计库中的函数的实现:(用户不需要知道的部分) 表现为一个
28、源文件库的这种实现方法称为信息隐藏,随机函数库的设计,库的功能 在9.1中,用到了随机生成0和1 在9.2中,用到了随机生成0和2 在自动出题中,用到了随机生成0和3及随机生成0到9 用一个函数概括:生成low到high之间的随机数int RandomInteger(int low, int high) 初始化函数RandomInit()实现设置随机数种子的功能,在9.1节中,设计了一个掷硬币的程序。该程序用到了随机数的一些特性。如果我们的工作经常需要用到随机数,我们可以把随机数的应用写成一个库。,接口文件,头文件的格式:与石头、剪子、布游戏中的头文件格式一样。头文件中,每个函数声明前应该有一
29、段注释,告诉用户如何使用这些函数。,库接口的设计,/文件:Random.h /随机函数库的头文件#ifndef _random_h #define _random_h/函数:RandomInit /用法:RandomInit() /作用:此函数初始化随机数种子 void RandomInit();/函数:RandomInteger /用法:n = RandomInteger(low, high) /作用:此函数返回一个low到high之间的随机数,包括low和high int RandomInteger(int low, int high);#endif,库的实现,库的实现文件和头文件的名字是
30、相同的。如头文件为Random.h,则实现文件为Random.cpp。 实现文件的格式: 注释:这一部分简单介绍库的功能。 include此cpp文件所需的头文件。每个实现要包含自己的头文件,以便编译器能检查函数定义和函数原型声明的一致性。 每个函数的实现代码。在每个函数实现的前面也必须有一段注释。,/文件:Random.cpp /该文件实现了Random库#include #include #include “Random.h“/函数:RandomInit /该函数取当前系统时间作为随机数发生器的种子 void RandomInit() srand(time(NULL); ,库的实现文件,/
31、 函数:RandomInteger / 该函数将0到RAND_MAX的区间的划分成high - low + 1 个子区间。 /当产生的随机数落在第一个子区间时,则映射成low。 / 当落在最后一个子区间时,映射成high。当落在第i个子区间时 /(i从0到high-low),则映射到low + i int RandomInteger(int low, int high) return low + (high - low + 1) * rand() / (RAND_MAX + 1);,库的实现文件,库的接口文件和实现文件总结,/接口文件:Random.h#ifndef _random_h #de
32、fine _random_h void RandomInit(); int RandomInteger(int low, int high); #endif,/文件:Random.cpp#include #include #include “Random.h“void RandomInit() srand(time(NULL);int RandomInteger(int low, int high) return low + (high - low + 1) * rand() / (RAND_MAX + 1); ,有了Random库就可以使用户远离系统的随机数生成器,而是直接调用库里的两个函数
33、,自顶向下的分解 模块划分 库的设计与实现 库的应用,库的应用 - 龟兔赛跑,乌龟和兔子的状态及发生概率:,龟兔赛跑解题思路,分别用变量tortoise和hare代表乌龟和兔子的当前位置 时间用秒计算 用随机数来决定乌龟和兔子在每一秒的动作,根据动作决定乌龟和兔子的位置的移动 跑道的长度设为70个点,第一层分解,main() int hare = 0, tortoise = 0, timer = 0; /timer是计时器,从0开始计时while (hare tortoise) cout “n hare wins!“;else cout “n tortoise wins!“; ,抽取函数,乌龟
34、在这一秒的移动距离:int move_tortoise();兔子在这一秒的移动距离:int move_hare();输出当前计时和兔子乌龟的位置void print_position(int timer, int tortoise, int hare);,模块划分,主模块移动模块 move_tortoise move_hare() 输出模块 print_position,主模块,#include “Random.h“ /包含随机数库 #include using namespace std;const int RACE_END = 70; /设置跑道的长度int move_tortoise()
35、; int move_hare(); void print_position(int, int, int);,int main() int hare = 0, tortoise = 0, timer = 0; RandomInit(); /随机数初始化cout tortoise) cout “n hare wins!n“;else cout “n tortoise wins!n“;return 0; ,如何用随机数模拟概率,以乌龟为例,它的三种情况和概率如下:为此我们可以生成0-9之间的随机数,当生成的随机数为0-4时,认为是第一种情况,5-6是第二种情况,7-9是第三种情况。这样就可以根据生
36、成的随机数确定发生的事件,Move模块,/ 文件名:move.cpp#include “Random.h“ /本模块用到了随机函数库int move_tortoise() int probability = RandomInteger(0,9); /产生0到9之间的随机数if (probability 5) return 3; /快走else if (probability 7) return -6; /后滑else return 1; /慢走 ,Move模块,int move_hare() int probability = RandomInteger(0,9);if (probabilit
37、y 2) return 0; /睡觉else if (probability 4) return -9; /大后滑else if (probability 5) return 14; /快走else if (probability 8) return 3; /小步跳else return -2; /慢后滑 ,Move模块,Print模块,/ 文件名:print.cpp#include using namespace std;void print_position(int timer, int t, int h) if (timer % 6 = 0) cout endl; /每隔6秒空一行cout timer t t t h n; ,模块划分,主模块void main()移动模块 int move_tortoise() int move_hare() 输出模块 void print_position(int timer, int t, int h),Random.h Random.cpp,运行结果,总结,本章介绍了 如何利用结构化程序设计的技术来解决一个大问题。 利用结构化程序设计的技术可以容易地将一个应用分成若干个独立的模块来构建整个程序。 如何在模块中保存内部状态 如何从程序中抽取出库以及如何设计和使用库,