垃圾“程序是怎樣練成的”——關於《C程序設計伴侶》第A章(六)


前文鏈接: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的作用域。

評析到此結束。
總體評價:沒有達到大學一年級學生課程設計的水平。

【附錄】

下面是該書提供的完整代碼,供大家對照閱讀。

 

View Code
#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 );
}

 

 

 


免責聲明!

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



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