下面来考察一下main()的总体结构。
29. main()30. {31. int numCards;32. int cards[52],playerPoints[2],dealerPoints[2],total[2];33. char ans;34. 35. do 36. { 37. initCardsScreen(cards,playerPoints,dealerPoints,total, &numCards);38. dealerGetsCard(&numCards,cards, dealerPoints);39. printf("\n");40. playerGetsCard(&numCards,cards,playerPoints); 41. playerGetsCard(&numCards,cards,playerPoints);42. do43. {44. ans = getAns("Hit or stand (H/S)?");45. if ( ans == 'H' )46. { 47. playerGetsCard(&numCards,cards,playerPoints);48. } 49. }50. while( ans != 'S' );51. 52. totalIt(playerPoints,total,PLAYER);53. do54. {55. dealerGetsCard(&numCards,cards,dealerPoints);56. }57. while (dealerPoints[ACEHIGH] < 17 );58. 59. totalIt(dealerPoints,total,DEALER);60. findWinner(total); 61. 62. ans = getAns("\nPlay again(Y/N)?"); 63. }64. while(ans=='Y');65. 66. return 0;67. }68.
main()函数中do-while语句循环体部分的含义是这样的
35. do 36. { /*21点游戏*/62. ans = getAns("\nPlay again(Y/N)?"); /*询问是否继续*/63. }64. while ( ans == 'Y' );
与下面写法相比,两者在逻辑上的结构差别很明显
do { /*21点游戏*/ } while( getAns("\nPlay again(Y/N)?") == 'Y' ); /*询问是否继续*/
前者循环体内要做两件事,更精确地说做1.5件事情,因为“询问是否继续”没有完全做利索,一脚门里一脚门外,拖泥带水。而后面的写法在循环体内只关注一件事情,这件事没有与另一件事情搅在一起,而是分处于do-while语句的不同部分,有个清楚的分界线。
即使从这个视角看,也不能赞成原来代码使用ans 变量的写法。既不应该把不同的事情(“21点游戏”和“询问是否继续”)搅在一起来做,也不应该把一件事(“询问是否继续”)分裂为分离的两部分在do-while语句的不同部分完成。
现在回到循环体内,看一下完整的21点游戏的模拟过程。
37. initCardsScreen(cards,playerPoints,dealerPoints,total, &numCards);
这条语句的主要目的是实现对程序相关数据的初始化。直观感觉是这个函数的参数太多了,通常标志着数据结构设计的失败。
16. void initCardsScreen(int cards[52],int playerPoints[2],17. int dealerPoints[2], int total[2], 18. int *numCards);
这个函数类型声明写得很不规范。[]内的52、2、2、2毫无必要,代码对齐格式也很成问题。应该为:
void initCardsScreen( int cards[] ,int playerPoints[] , int dealerPoints[] , int total[] , int *numCards ) ;
同理,对应的函数定义中
69. void initCardsScreen( int cards[52],int playerPoints[2],70. int dealerPoints[2], int total[2], 71. int *numCards )72. {73. int sub,val = 1 ;74. char firstName[15];75. *numCards=52;76. 77. for(sub=0;sub<=51;sub++)78. {79. val = (val == 14) ? 1 : val;80. cards[sub] = val;81. val++; 82. }83. 84. for(sub=0;sub<=1;sub++)85. { 86. playerPoints[sub]=dealerPoints[sub]=total[sub]=0;87. }88. dispTitle();89. 90. if (askedForName==0)91. { 92. printf("What is your first name?");93. scanf(" %s",firstName);94. askedForName=1;95. printf("Ok, %s,get ready for casino action!\n\n",firstName);96. getchar();97. }98. return; 99. }
中,函数的头部同样应该做类似修改
void initCardsScreen( int cards[],int playerPoints[], int dealerPoints[], int total[], int *numCards )
因为形参不可能是数组,[]内的内容无论是多少编译器都将予以忽略。写了也是白写,写它作甚?
numCards这个形参的名称甚劣,它本来是一个指针,但却居然与它所指向的变量同名。这种写法说明代码作者缺乏最基本的编程素养。
initCardsScreen()函数的功能据代码作者描述是初始化表示52张牌的数组(写入4套1~13),清屏并显示标题。从这个功能描述就足以认定这个函数的设计非常失败——它的功能太多了。函数应该只做一件事。
函数体中的
75. *numCards=52;
一句,非常煞有介事,因为这是完全用不着的。只要在main()中的do-while语句的循环体内中写一句
numCards = 52 ;
就可以了,用函数调用的方法为变量numCards初始化是根本不值得的。
读到这里,不难发现在main()中的这些变量定义的位置也不恰当,这些变量应该在do-while语句之内定义。
do { int numCards = 52 ; int cards[52],playerPoints[2],dealerPoints[2],total[2]; /*21点游戏*/ } while( getAns("\nPlay again(Y/N)?") == 'Y' ); /*询问是否继续*/
变量的定义应该尽量局部化。
77. for(sub=0;sub<=51;sub++)78. {79. val = (val == 14) ? 1 : val;80. cards[sub] = val;81. val++; 82. }
这个写法很业余,而且51是一个Magic Number,14也是。专业一点的写法是
for ( sub = 0 ; sub < 52 ; sub++ ) { cards [ sub ] = sub % 13 + 1 ; }
所以val这个变量也同样是多余的。
84. for(sub=0;sub<=1;sub++)85. { 86. playerPoints[sub]=dealerPoints[sub]=total[sub]=0;87. }
“sub<=1”的写法很业余,专业人员一般写为“ sub < 2 ”。
“playerPoints[sub]=dealerPoints[sub]=total[sub]=0;”这个写法通常会受到非议,比较讲究风度的程序员一般这样写:
playerPoints[sub]= dealerPoints[sub]= total[sub]= 0 ;
还应该注意到的是,这几个数组的各个元素由于初始化为0,其实也不用通过函数调用这么复杂的方法来实现,最简洁初始化方法是在main()中直接初始化:
do { int numCards = 52 ; int cards[52] , playerPoints[2] = { 0 } , dealerPoints[2] = { 0 } , total[2] = { 0 } ; /*21点游戏*/ } while( getAns("\nPlay again(Y/N)?") == 'Y' ); /*询问是否继续*/
结论不难得出:变量定义位置不当,会让代码笨拙无比,会给自己带来很多麻烦。现在也可以确认initCardsScreen()这个函数参数过多,实际上只需要对cards[]数组初始化。
88. dispTitle();
这行代码的作用据作者说是清屏,但是这个函数调用在initCardsScreen()这个函数中明显是一种错位。从这个函数的定义看
215. void dispTitle(void)216. {217. int i = 0 ;218. while(i<25)219. { 220. printf("\n");221. i++; 222. }223. printf("\n\n*Step right up to the Blackjack tables*\n\n");224. return ;225. }
其作用是输出了25个新行字符,然后输出字符串"\n\n*Step right up to the Blackjack tables*\n\n"。注意,此时光标移动到屏幕的左下方,而真正的清屏,光标是在屏幕的左上方。所以作者认为自己写了一个“比通常使用的清屏函数更加特别的清屏函数”,显然只是一种一厢情愿的错觉,他写得太“特别”了,已经特别到了算不上是清屏函数的程度了。
即使纯粹从写法上来讲,这个函数写得也很拙劣。下面的写法要整洁干净得多。
void dispTitle( void ){ int i ; for( i = 0 ; i < 25 ; i ++ ) { putchar('\n'); } puts("\n\n*Step right up to the Blackjack tables*\n");}
现在回到initCardsScreen()函数定义部分。
90. if (askedForName==0)91. { 92. printf("What is your first name?");93. scanf(" %s",firstName);94. askedForName=1;95. printf("Ok, %s,get ready for casino action!\n\n",firstName);96. getchar();97. }
这段代码的毛病很多。首先,这段代码根本就不应该出现在initCardsScreen()函数定义之中,因为这和初始化没关系。
其次,即使出现,也应该像dispTitle()那样抽象为一个函数为好。 第三,现在终于弄清代码作者设置askedForName这个外部变量的用意了,但这种用意不但愚蠢而且笨拙。因为这个事情完全应该安排在main()中进行,大致的写法如下int main(void){ char firstName[15]; dispTitle(); /*这里读入firstName*/ do{ /*21点游戏*/ }while( getAns("\nPlay again(Y/N)?") == 'Y' ); /*询问是否继续*/ return 0;}
完全没有必要笨拙且愚蠢地动用危险的外部变量,那种写法完全是初学者的水准,写在书上给其他初学者做为示范是极其不负责任的行为。
看到这里有些无语……,还能说什么呢?用星爷的话来形容,“失败中的失败”。
第四,作者声称
96. getchar();
会招致编译器的警告,并要读者心安理得地忽视这个警告(ignore compiler warning here)。说实话,我见过这种由于单独调用getchar()函数而不使用其返回值的警告,但是要读者忽视警告则是一种误导和教唆。
这里如果有警告,应该用下面方法去除:(void) getchar();