垃圾“程序是怎样练成的”——关于《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 );
}

 

 

 

posted @ 2012-12-20 00:30  garbageMan  阅读(2371)  评论(14编辑  收藏  举报