下面來考察一下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. do 43. { 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. do 54. { 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();