前文鏈接:http://www.cnblogs.com/pmer/archive/2012/12/18/2823626.html
【樣本】
【評析】
代碼沒多大問題。問題出在第二行注釋:“參數files和count是保存txtfile結構體數據的數組”。這兩個參數根本不是數組。
更嚴重的問題是,這個輸出根本沒有完成最初要求的功能:“按照從大到小的順序將這些文件名輸出到屏幕和結果文件”(P272)。參見 垃圾“程序是怎樣煉成的”——關於《C程序設計伴侶》第A章(一)。MVP寫到了最后完全忘記當初要做的是什么了。
【樣本】
【評析】
不懂裝懂的聳人聽聞。
所謂“否則,會造成嚴重的內存泄漏問題”,“結果會導致被程序占用的內存資源越來越多,為系統的穩定運行帶來隱患”是胡扯和誤導。程序結束,它所占用的內存資源會由操作系統釋放,並不會導致所謂的內存泄漏問題。
通常所說的內存泄漏(memory leak )是指程序長時間運行情況下失去對所申請內存的控制,這種情況持續增長到一定程度會帶來嚴重問題。
當然,這並不是反對主動釋放所申請的內存。
【樣本】
// 清理動態申請的內存 void clean(txtfile* files, int count) { // 循環遍歷所有文件的鏈表 for(int i = 0;i<count;++i) { // 讓head指向鏈表的開始位置 word* head = files[i].list; // 判斷鏈表是否為空 while(NULL != head) { // 將首結點作為當前結點 word* cur = head; // 然后,將下一個結點作為新的首結點 head = cur->next; // 釋放當前結點動態申請的內存 free(cur); cur = NULL; } } } // 主函數 int main() { // 處理問題…
// 打掃戰場 clean(files,filecount); return 0; }
【評析】
這段代碼的毛病有兩個:第一,釋放鏈表所占用內存,應該在最后把鏈表標注為空,即函數最后應該
files[i].list=NULL;
否則,活干得不干凈。
其次,free(cur);之后的
cur = NULL;
畫蛇添足,完全沒有意義,只是一種東施效顰的寫法。因為在下一輪循環中馬上就會
cur = head ;
如果循環結束,就更沒必要考慮cur的取值問題了,因為那時已經離開了cur的作用域。
評析到此結束。
總體評價:沒有達到大學一年級學生課程設計的水平。
【附錄】
下面是該書提供的完整代碼,供大家對照閱讀。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <stdbool.h> #include <math.h> // 單詞結點結構體 typedef struct _word { char text[30]; // 保存單詞的字符數組 int count; // 單詞的個數 struct _word* next; // 指向鏈表中的下一個單詞 } word; // 表示文件的結構體 typedef struct _txtfile { char name[128]; // 文件名 char text[1024*128]; // 文件的文本內容 word* list; // 保存單詞的鏈表 int total; // 單詞總數 float correlation; // 關鍵詞在文件中的詞頻 } txtfile; // 讀取文件子模塊 void readfile(txtfile* file) { // 參數合法性檢查 if(NULL == file) return; // 以只讀方式打開txtfile結構體關聯的文件 FILE* fp = fopen(file->name,"r"); // 如果打開成功,則讀取其中的文本內容 if( NULL != fp ) { // 逐行讀取文件中的文本內容 char line[256]; while( NULL != fgets(line,256,fp)) { // 通過將讀取得到的字符串連接到已有字符串的末尾, // 實現將讀取的內容添加到txtfile結構體中的字符數組中進行保存 strcat(file->text,line); } // 讀取完畢,關閉文件 fclose(fp); } } // 清理文本 // 將其中的無效字符替換為空格 void cleantext(char* text) { int i = 0; // 遍歷訪問字符串中的每一個字符 while(i<strlen(text)) { // 調用標准庫函數isalnum(), // 判斷當前字符是否是無效字符(不是字母或數字) if(!isalnum(text[i])) // { text[i] = ' '; // 如果是無效字符,替換為空格 } ++i; // 檢查下一個字符 } } // 切分單詞模塊 char* cutword(char* text,char* word) { // 目標字符串中是否包含字符 // 以此來判斷是否需要忽略空白字符, // 如果遇到空格符,則表示這是單詞開始之前的空白字符,需要忽略, // 反之,則表示這是單詞之后的空白字符,整個單詞切分完畢, // 可以結束本次單詞切分 bool continchar = false; // 初始狀態為false,表示目標字符串中還沒有字符 int i = 0; // 源字符串中的索引 int w = 0; // 目標字符串中的索引 // 從源字符串的開始位置,逐個字符向后遍歷訪問 while(i<strlen(text)) { // 判斷當前字符是否是空格符或者換行符 if((' ' == text[i]) || ('\n' == text[i])) { // 如果目標字符串中已經包含字符,也就是說這是單詞末尾位置的 // 空格符,例如,“Jiawei ”,表示單詞結束,所以用break結束循環 if(continchar) break; // 反之,則表示這是單詞開始之前的空格字符,例如,“ Jiawei”, // 所以用continue繼續循環,向后繼續檢查字符 else { ++i; continue; } } else { // 如果遇到有效字符,則將其從源字符串text中 // 復制到目標字符串word中。 continchar = true; word[w] = text[i]; ++w; ++i; } } // 在目標字符串的末尾位置添加字符串結束符 word[w] = '\0'; // 將源字符串的指針向后移動i個字符,作為下一次切分的開始位置 return text + i; } // 根據切分得到的單詞創建單詞結點 word* createnode(char* text) { // 為結點申請內存 word* node = malloc(sizeof(word)); // 新添加的結點肯定是鏈表的尾結點,所以其next為NULL, // 不指向任何結點 node->next = NULL; // 將切分得到的單詞復制到結點保存 strcpy(node->text,text); // 初始單詞數為1 node->count = 1; // 返回新創建的結點 return node; } // 在head指向的鏈表中查找key所指向的字符串 word* findnode(word* head,char* key) { // 遍歷鏈表的所有結點 word* node = head; while(NULL!=node) { // 判斷當前結點的內容是否與要查找的內容相同 if(0 == strcmp(node->text,key)) { // 如果相同,則返回當前結點 return node; } node = node->next; } // 在鏈表中沒有找到,返回NULL return NULL; } // 數據預處理 // 將txtfile結構體中的文本內容切分成單詞並保存在鏈表中, // 同時統計每個單詞的個數和單詞總數 void parseword(txtfile* file) { // 需要處理的文本內容 char* text = file->text; // 保存單詞的鏈表,初始狀態為空 file->list = NULL; // 單詞總數初始為0 file->total = 0; // 前一個結點的初始狀態為NULL word* pre = NULL; // 利用清理文本子模塊清理文本中的無效字符 cleantext(text); while(true) { char wd[30] = ""; // 利用切分單詞子模塊, // 從文本內容text中切分出單詞保存到wd字符數組 text = cutword(text,wd); // 判斷是否成功切分得到單詞 if(strlen(wd)>0) { // 成功切分單詞,文件的單詞總數加1 file->total += 1; // 查找當前單詞wd是否已經存在於文件的單詞鏈表file->list中, // 如果存在,則返回指向這個結點的word*指針,否則返回NULL word* exist = findnode(file->list,wd); // 如果當前單詞沒有在文件的單詞鏈表中 if( NULL == exist ) { // 調用創建單詞子模塊,創建新的單詞結點 word* node = createnode(wd); // 判斷是否有前結點 if(NULL == pre) { // 如果沒有前結點,則表示這是鏈表的第一個結點, // 所以將文件的鏈表指針指向這個結點 file->list = node; } else { // 如果有前結點,則將當前結點連接到前結點 pre->next = node; } // 將當前結點作為下一個結點的前結點 pre = node; } else { // 如果當前單詞已經存在於鏈表中, // 只需要將這個單詞的個數加1即可,無需添加新的單詞結點 exist->count += 1; } } else // 相對於if(strlen(wd)>0) { // 如果無法成功切分單詞,表示整個文本內容 // 已經切分完畢,用break關鍵字退出循環 break; } } } // 計算詞頻模塊的實現 // 參數files和count是保存txtfile結構體的數組指針和元素個數, // keyword是要計算詞頻的關鍵詞 void countkeyword(txtfile* files,int count,char* keyword) { // 利用for循環,計算關鍵詞在每一個文件中的詞頻 for(int i = 0; i < count;++i) { // 在當前文件中查找關鍵詞結點 word* keynode = findnode(files[i].list,keyword); // 如果找到結點,則計算詞頻 if(NULL != keynode) { // 利用單詞的個數除以文件的單詞總數計算詞頻 files[i].correlation = keynode->count/(float)files[i].total; } else // 如果沒有找到,詞頻為0 { files[i].correlation = 0.0f; } } } // 比較規則函數 int cmp(const void* a,const void* b) { // 將void*類型的參數轉換為實際的txtfile*類型 const txtfile* file1 = (txtfile*)a; const txtfile* file2 = (txtfile*)b; // 比較txtfile結構體的詞頻 if(fabs(file1->correlation - file2->correlation) < 0.001) { return 0; } else if(file1->correlation > file2->correlation) { return 1; } else { return -1; } } // 文件排序模塊的實現 void sortfiles(txtfile* files,int count) { // 調用qsort()函數對數組進行排序 qsort(files,count,sizeof(txtfile),cmp); } // 數據輸出模塊的實現 // 參數files和count是保存txtfile結構體數據的數組, // keyword是本次查詢的關鍵詞 void printfiles(txtfile* files,int count,char* keyword) { // 輸出本次查詢的關鍵詞 printf("the keyword is \"%s\"\n",keyword); // 輸出這個關鍵詞在各個文件的詞頻 puts("the correlations are "); for(int i = 0; i < count;++i) { printf("%s %.4f\n",files[i].name,files[i].correlation); } } // 清理動態申請的內存 void clean(txtfile* files, int count) { // 循環遍歷所有文件的鏈表 for(int i = 0;i<count;++i) { // 讓head指向鏈表的開始位置 word* head = files[i].list; // 判斷鏈表是否為空 while(NULL != head) { // 將首結點作為當前結點 word* cur = head; // 然后,將下一個結點作為新的首結點 head = cur->next; // 釋放當前結點動態申請的內存 free(cur); cur = NULL; } } } // 主函數 int main() { // 定義需要處理的文件數 const int filecount = 5; // 構造需要處理的文件為一個files數組 // 在這里,給定文件名以及文本內容的 // 初始值對files數組中的txtfile結構體數據進行初始化 txtfile files[] = {{"text1.txt",""}, {"text2.txt",""}, {"text3.txt",""}, {"text4.txt",""}, {"text5.txt",""}}; // 循環讀取files數組中的5個文件 for(int i = 0;i<filecount;++i) { // 將文件的內容讀取到txtfile結構體的text字符數組中 readfile(&files[i]); parseword(&files[i]); } // 處理問題… while(true) { // 輸入關鍵詞… puts("please input the keyword:"); char keyword[30] = ""; // 獲取用戶輸入的關鍵詞 scanf("%s",keyword); // 如果用戶輸入的是“#”,則表示查詢結束退出循環 if(0 == strcmp(keyword,"#")) break; //printf("%s",files[0].list->text); // 計算關鍵詞在各個文件中的詞頻 countkeyword(files,filecount,keyword); //printf("==%d",files[0].total); // 按照關鍵詞在各個文件中的詞頻,對文件進行排序 sortfiles(files,filecount); // 輸出排序完成的數組和關鍵詞 printfiles(files,filecount,keyword); } // 打掃戰場 clean(files,filecount); return 0; }
【重構】
數據結構:
因為要按照詞頻“從大到小的順序將這些文件名輸出到屏幕和結果文件”,所以設計如下數據結構建立兩者之間的關聯。
typedef struct { char *filename ; double word_freq ; } statistics_t ;
因為一共有5個文件,所以這樣的數據構成了一個數組
statistics_t files[] = { {"file1.txt"}, {"file2.txt"}, {"file3.txt"}, {"file4.txt"}, {"file5.txt"}, };
根據文件的名字就可以求出關鍵詞的詞頻。
關鍵詞應該限定長度,這是常規做法。比如百度搜索就有類似的限制(印象中最長是28個漢字)。因此
#define MAX_LEN 32
但是存儲空間應預留字符串結尾的nul character的空間
#define ROOM_SIZE (MAX_LEN + 1)
因此,存儲關鍵詞的數據結構為
char keyword [ ROOM_SIZE ];
此外由於要將結果輸出到“結果文件”,所以
FILE *p_save ;
用於將排序結果輸出到文件。
算法:
#define MAX_LEN 32 #define ROOM_SIZE (MAX_LEN + 1) typedef struct { char *filename ; double word_freq ; } statistics_t ; int main( void ) { statistics_t files[] = { {"file1.txt"}, {"file2.txt"}, {"file3.txt"}, {"file4.txt"}, {"file5.txt"}, }; char keyword [ ROOM_SIZE ]; FILE *p_save ; //輸入關鍵詞 //統計詞頻 //排序 //輸出 return 0; }
“輸入關鍵詞”部分的實現:
puts("輸入關鍵詞:"); scanf("%32s", keyword ) ;
這里的“32”是為了保證在輸入太長時不至於寫到keyword數組之外。這個“32”也可以用宏MAX_LEN來表達:
#define FOMR(x) FOMR_(x) #define FOMR_(x) "%"#x"s" scanf(FOMR(MAXLEN), keyword ) ;
這樣顯得更優雅一些。
當然,整個“輸入關鍵詞”部分也可以函數實現,那樣更具有通用性也更為優雅,但實現起來要稍微更費事些。
“統計詞頻”需要files數組相關數據及keyword 作為參數
void stat_files( statistics_t [] , size_t , const char * ); //統計詞頻 stat_files( files , sizeof files / sizeof *files , keyword ); void stat_files( statistics_t f_a[] , size_t size , const char *key ) { int i ; for( i = 0 ; i < size ; i ++ ) ( f_a + i )-> word_freq = stat_file( (f_a + i)->filename , key ); }
stat_files()函數把統計一組文件詞頻的任務分解為對單個文件統計詞頻的任務,stat_file()函數的定義為
double stat_file( const char *filename , const char * key ) { FILE *fp = my_fopen( filename , "r" ); char temp [ ROOM_SIZE ]; int num_word = 0 , num_key = 0 ; while( fscanf( fp , "%*[^ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]") , fscanf( fp , "%32[ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]" , temp ) != EOF ) //單詞定義為連續的字母和數字 { num_word ++ ; if( strcmp ( temp , key ) == 0 ) num_key ++ ; } fclose(fp); return num_key == 0 ? 0. : (double)num_key/(double)num_word; }
它的核心部分就是從輸入流中直接讀出單詞。樣本代碼中對“單詞”的定義,無非是連續的字母和數字字符。所以首先從輸入流中
fscanf( fp , "%*[^ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]")
它會先“讀掉”所有的非字母和數字字符,並且不予存儲。而
fscanf( fp , "%32[ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]" , temp )
則讀取連續的連續的字母和數字字符並將之存儲於temp 這個char [32+1]當中。每讀到一個單詞,單詞總數加1(num_word ++);如果所讀到的單詞是關鍵詞,則關鍵詞數加1(num_key ++)。
我曾經一度以為
fscanf( fp , "%*[^ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]") , fscanf( fp , "%32[ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]" , temp ) != EOF
這個逗號表達式應該簡潔地寫為
fscanf( fp , "%*[^ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]\ %32[ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz0123456789]" , temp ) != EOF
后來發現這個是個愚蠢的錯誤。這個地方似乎也只能寫成逗號表達式了。
排序和輸出是幼兒園孩子都能寫上來的代碼,這里就不詳細展開說了。下面是完整的代碼。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_LEN 32 #define ROOM_SIZE (MAX_LEN + 1) #define FOMR(x) FOMR_(x) #define FOMR_(x) "%"#x"s" #define ALPHA "ABCDEFGHIJKLMNOPQRSTUVWXZabcdefghijklmnopqrstuvwxyz" #define NUMBER "0123456789" #define SKIP "%*[^" ALPHA NUMBER "]" #define READ(x) READ_(x) #define READ_(x) "%" #x "[" ALPHA NUMBER "]" typedef struct { char *filename ; double word_freq ; } statistics_t ; void stat_files( statistics_t [] , size_t , const char * ); double stat_file( const char * , const char * ); FILE* my_fopen(const char * , const char * ); int cmp(const void *, const void *) ; void output( FILE* , statistics_t [] , size_t ); int main( void ) { statistics_t files[] = { {"file1.txt"}, {"file2.txt"}, {"file3.txt"}, {"file4.txt"}, {"file5.txt"}, }; char keyword [ ROOM_SIZE ]; FILE *p_save ; //輸入關鍵詞 puts("輸入關鍵詞:"); scanf(FOMR(MAX_LEN), keyword ) ; //scanf("%32s", keyword ) ;比較優雅的寫法 //統計 stat_files( files , sizeof files / sizeof *files , keyword ); //排序 qsort( files , sizeof files / sizeof *files , sizeof *files , cmp ); //輸出 output( stdout , files , sizeof files / sizeof *files ); p_save = my_fopen( "save.txt" , "w" ) ; output( p_save , files , sizeof files / sizeof *files ); return 0; } void output( FILE *out , statistics_t f[] , size_t size ) { size_t i ; for( i = 0 ; i < size ; i ++ ) fprintf( out , "%s : %f\n" ,(f + i)->filename , (f + i)->word_freq ); } int cmp(const void * f1, const void *f2) { if( ((statistics_t *)f1)-> word_freq > ((statistics_t *)f2)-> word_freq ) return 1; if( ((statistics_t *)f1)-> word_freq < ((statistics_t *)f2)-> word_freq ) return -1; return 0; } FILE* my_fopen(const char *filename , const char * mode ) { FILE *fp = fopen( filename , mode ); if( fp == NULL ) exit(1); return fp; } double stat_file( const char *filename , const char * key ) { FILE *fp = my_fopen( filename , "r" ); char temp [ ROOM_SIZE ]; int num_word = 0 , num_key = 0 ; while( fscanf( fp , SKIP ) , fscanf( fp , READ(MAX_LEN) , temp ) != EOF ) //單詞定義為連續的字母和數字 { num_word ++ ; if( strcmp ( temp , key ) == 0 ) num_key ++ ; } fclose(fp); return num_key == 0 ? 0. : (double)num_key / (double)num_word ; } void stat_files( statistics_t f_a[] , size_t size , const char *key ) { int i ; for( i = 0 ; i < size ; i ++ ) ( f_a + i )-> word_freq = stat_file( (f_a + i)->filename , key ); }