垃圾“程序是怎样练成的”——关于《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的作用域。
评析到此结束。
总体评价:没有达到大学一年级学生课程设计的水平。
【附录】
下面是该书提供的完整代码,供大家对照阅读。
#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 ); }