劣質代碼評析——《寫給大家看的C語言書(第2版)》附錄B之21點程序(三)


  下面來考察一下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();

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM