zoukankan      html  css  js  c++  java
  • 垃圾“程序是怎样练成的”——关于《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 );
    }
    
    
  • 相关阅读:
    C# DataGridview转换为DataTable
    未在本地计算机上注册“microsoft.ACE.oledb.12.0”提供程序
    ORM(Object Relational Mapping)框架
    C#开发小技巧
    Windows系统中Oracle11g R2 版本数据库卸载
    world特殊控制符输入
    Java中生成帮助文档
    Java类——JDBC链接、并操作MySQL数据库
    Java——实现对密码进行MD5加密
    HTTP协议详解
  • 原文地址:https://www.cnblogs.com/pmer/p/2825733.html
Copyright © 2011-2022 走看看