【題目】
西安交大的計算機專業2011級有6個班,每個班的人數不等,但最多不超過100個。現在期末考試結束后,老師要統計每個班的平均成績並將各個班級的成績按照班級平均成績的次序打印出來(也就是按照從低到高的順序,先打印平均成績較低的班級的成績)。老師希望你幫他寫一個程序來完成這一工作。
——陳良喬 ,《C程序設計伴侶》,人民郵電出版社,2012年10月,p104
【評析】
作為例題或練習來說,這個題目很垃圾。最垃圾的地方在於那個“100”。這非但與實際不合,而且使得代碼基本無法測試,因為通常做練習時不可能真的輸入600個數據進行測試。既然不能較完整地進行測試,代碼的正確性就難免大打折扣。
【樣本】
- 向二維數組輸入數據
定義二維數組之后,接下來的任務,自然是利用scanf()函數將數據輸入到這個數組中。因為這是一個二維數組,所以我們需要一個嵌套的for語句,第一層for語句在數組的第一個維度循環(從0到5),第二層for語句則在數組的第二個維度循環(從0到99)。這樣才有可能訪問到數組中的所有數據。因為每個班級的人數並不一定是100人,所以我們還需要在輸入的時候用條件結構對輸入數據進行判斷,如果輸入數據是0,則表示這個班級的輸入結束,可以開始下一個班級的循環。經過這樣的分析,我們可以將程序的輸入數據部分實現如下:
——陳良喬 ,《C程序設計伴侶》,人民郵電出版社,2012年10月,p104
【評析】
“如果輸入數據是0,則表示這個班級的輸入結束”,這其實是一個很拙劣的假設,因為根據常識分數是有可能為0的。除此之外,這還會帶來其他問題,導致代碼錯誤。
【樣本】
1. #include <stdio.h> 2. #include <stdLib.h> 3. #include <memory.h> 4. 5. int main() 6. { 7. // 定義保存批量數據的二維數組, 8. // 並用memset()函數完成數組的初始化 9. const int classnum = 6; 10. const int stnum = 100; 11. int scores[classnum][stnum]; 12. memset(scores,0,classnum*stnum*sizeof(int)); 13. 14. // 利用for語句完成數據的輸入 15. // 逐個班級循環 16. for(int i = 0; i < classnum;++i) 17. { 18. printf("please input the scores of class %d:\n",i+1); 19. // 逐個學生循環 20. for(int j = 0; j < stnum; ++j) 21. { 22. // 將輸入的數據保存到scores[i][j] 23. scanf("%d",&scores[i][j]); 24. 25. // 判斷剛剛輸入的數據是否為0, 26. // 如果為0,則利用break結束本層循環 27. if(0 == scores[i][j]) 28. break; 29. } 30. } 31. 32. // … 33. 34. return 0; 35. }
在這里,我們首先定義了保存批量數據用的scores這個二維數組,然后利用一個嵌套的for語句將輸入的數據保存到數組中。for語句會逐個遍歷數組中的數據,讓我們可以利用scanf()函數將輸入數據保存到數組元素(scores[i][j])中。因為一些特殊的設定(以0表示輸入結束),我們還需要利用條件結構對輸入數據進行判斷(if(0 == scores[i][j])),如果發現輸入數據是0,則結束本層循環,開始下一個班級的成績輸入。
——陳良喬 ,《C程序設計伴侶》,人民郵電出版社,2012年10月,p104~105
【評析】
這段代碼的第一個問題是
2. #include <stdLib.h>
不難注意到其中的<stdlib.h>被寫成了<stdLib.h>,這在某些環境下會造成錯誤。
9. const int classnum = 6; 10. const int stnum = 100; 11. int scores[classnum][stnum];
這是一種半吊子寫法。為什么這么說呢?
首先,完全沒必要!對這個問題來說沒有任何必要用變量給出數組的尺寸,因為6和100都是問題給出的常數。定義數組只要
int scores[6][100];
對於初學者來說就是很好的寫法了。不需要把這個數組定義成復雜的VLA(variable length array)。
VLA只是C99的特性,很多編譯器都不支持,例如微軟的VC++6.0。而且在C語言的最新標准C11中,VLA並不是編譯器必須支持的,它只是一種Conditional feature。況且使用VLA,使得代碼喪失了方便地進行初始化的可能性(VLA不可以初始化)。
如果是為了避免在定義數組時使用6和100這樣的MagicNumber,其實只要使用宏定義就完全可達到目的:
#define CLASSNUM 6 #define STNUM 100 int scores[CLASSNUM][STNUM];
這里使用了預處理命令。
有一些人對預處理極其反感,反感的原因之一是這里的兩個符號常量在預處理之后依然被替換為字面常量,在調試時可能多有不便。這個問題同樣容易解決,只要
enum { CLASSNUM = 6 , STNUM = 100 }; int scores[CLASSNUM][STNUM];
這種寫法克服了使用宏定義的缺點和不足。
12. memset(scores,0,classnum*stnum*sizeof(int));
這條語句驢唇不對馬嘴。它的目的是想實現“完成數組的初始化”,但是由於scores中有classnum*stnum個int類型數據,所以所謂的初始化的含義是向scores寫classnum*stnum個int類型的初始數據。而12行的含義則是用classnum * stnum * sizeof(int)個(unsigned char)0填充scores這塊空間,和數組的初始化完全不是一回事。
12行本來是不必要的,但是代碼作者有一種“初始化強迫症”——不管有沒有必要都初始化。然而使用VLA又將使用常規方法初始化的路作繭自縛地給堵上了,於是自作聰明地使用不倫不類的memset()。和這種吃力不討好且概念錯誤的方法相比,前面任何一種非VLA方案,都可以簡單輕松地用
的方法實現將scores所有int類型元素初始化為0值的功能。
14. // 利用for語句完成數據的輸入 15. // 逐個班級循環 16. for(int i = 0; i < classnum;++i) 17. { 18. printf("please input the scores of class %d:\n",i+1); 19. // 逐個學生循環 20. for(int j = 0; j < stnum; ++j) 21. { 22. // 將輸入的數據保存到scores[i][j] 23. scanf("%d",&scores[i][j]); 24. 25. // 判斷剛剛輸入的數據是否為0, 26. // 如果為0,則利用break結束本層循環 27. if(0 == scores[i][j]) 28. break; 29. } 30. }
這個循環嵌套結構用於輸入。它的如意算盤是把scores這個二維數組填充成類似字符串那種結構,即對於每個一位數組scores[i]中都有一個0值元素表示它本身及其后面的元素無效。但是作者忘記了,在有些情況下這根本不可能。比如,一口氣輸入classnum* stnum個非0數據,scores中就不可能存在值為0的元素。這一點對於這個題目后面的代碼來說是一個巨大的漏洞。
(待續)