背景
人生第一次读项目源码。
这是SIM查重工具,由大牛Dick Grune创造。文档和源码都可以在他的网站里找到。Paper是.ps文件,需要下载Adobe Acrobat打开阅读,也可以用Acrobat把它导出成pdf,用习惯的pdf阅读器看。
关于怎么运行他的源码,我的上一篇博客记录了配置MinGW的过程。当然,只是使用的话,直接下exe包就行了。
前段时间云里雾里地读了一下SIM提供的三个文档,Manual,Paper,Technique Report,然后这几天一头扎进源码里面,希望细节性地了解了它的实现以后,试图做一点创新,以此作为我大创项目的主要工作(这都三月份了,大创四月份就要结题了,说实话慌得很)。
第一次读源码,没有经验,就一头扎到main里面一层层地读下去了,过程中有一些心得随缘地记录一下。
前言
光对着源码看几秒钟就走神,所以一边写一边看来让自己注意力集中,这就是一篇笔记,质量就不强求了。
因为是一头扎到main()里面去看的,所以就从main讲起吧。
初看sim.c
main()在sim.c里面,所以我就从sim.c开始看起了。有一个感受,就是开始写这篇博客的时候看过的函数已经遍布五六七八个文件了,但是每个文件的代码量其实都在差不多的范围里,差不多都是200到400行的样子,这里面是有不少封装的思想。
oplist[]
这个文件里main()之前的内容并不多,最显眼的还是那一长串的oplist,这个是选项列表,因为sim工具给用户提供了许多的设置选项,可以根据不同需要进行查重,比如哪些文件不需要相互进行比较(old文件)、要不要按百分比显示查重率、百分之多少以上的重复率输出等等。
这个指令列表预设了这些指令,它是stuct option*类型,关于这个结构体,需要注意的就是里面有一个enum类型的变量,它指示的是这个指令后面所跟的参数,比如-r后面需要跟一个整型参数,表示MinRunSize被调整成多少。
读入参数指令
在Read_Input_Files(argc, argv)被调用之前,其实都是一些准备的工作,比如把版本号设置出来,保留progname,读入从命令行得到的参数,调用do_options,把这些参数都存到options列表当中去,用于后面检查哪些参数是被设定的。可以说主要就是对输入的参数指令进行检查,再做一些处理,比如如果接收到-v这个参数,就可以直接输出版本号结束程序了。
这里面应该说,感受到真正的项目和我之前做的算法题,还有我做的一些大作业比起来,它的出错处理就做得很精细了,在这里的准备工作里面,就对各种命令之间的冲突做了判断,如果有所错误,会进行相应的报错,然后退出。
读入文件
主要是Read_Input_Files的调用。
其实就是这一块略略读完之后,激发了我写博客的动力。之前也就碰到个结构体,是option,到这里它开始了层层调用,并且出现了text这个结构体,我怕不写一下博客,过几天回来看就忘了这一块在干什么了。
/*这是text类型的定义*/
struct text {
const char *tx_fname; /* the file name */
size_t tx_start; /* index of first token in Token_Array[]
belonging to the text */
size_t tx_limit; /* index of first position in Token_Array[]
not belonging to the text */
/* 就表示这一段在Token_Array里面是[tx_start, tx_limit) */
int tx_EOL_terminated; /* Boolean */ /*暂时不知道干嘛的*/
struct position *tx_pos;/* list of positions in this file that are
part of a chunk; sorted and updated by
Pass 2
*/
};
struct position {
/* position of first and last token of a chunk */
struct position *ps_next;
int ps_type; /* first = 0, last = 1, for debugging */
size_t ps_tk_cnt; /* in tokens; set by add_run()
in Read_Input_Files() */ /*最后用来输出匹配数量*/
size_t ps_nl_cnt; /* same, in line numbers;set by Retrieve_Runs(),
used by Print_Runs(), to report line numbers
*/ /* 最后用来输出匹配行号 */
};
其实感觉text已经像是被封装成一个类了,不过只是封装的感受强烈,OOP当中多态和继承的思想它没有用到(也不需要吧)。目前对text的理解,就是它其实是指示了,在Token_Array当中,当前这个文件从第几号Token开始,从第几号Token结果。position的属性要等到pass2再来详看作用了。
Read_Input_File的调用一层又一层,尤其是“打开文件”这一个功能,后面还有流式文件的封装,给我绕晕了。琢磨了一段时间发现没啥效果,我就“抓大放小”地把后面几层调用先给放了。总的来说,这个模块就是把打开的文件中的token识别出来,存放在Token_Array数组当中。追溯获得token的方法,就是通过flex的yylex()来获得。词法分析和token的存储还蛮重要的,第一遍看没有能特别看得细,后面可能需要回来细看。
Compare_Files()
main()当中对这个函数的注释是"turns texts into runs",我在看文档的时候就对run这个单位有些迷惑,这部分明显是真正开始核心工作的地方。
这个函数又由三个函数嵌套形成:
void
Compare_Files(void) {
Make_Forward_References();
compare_texts();
Free_Forward_References();
}
Make_Forward_References()
从TechnReport里大概了解到Foward_References,它是加速匹配的一种方法,但是看的时候对使用last_index[]对Foward_References进行更新的过程有所迷惑。这里方便起见,翻译一下TechnReport,也是注释里,对这个过程的描述。
文件的比较方法是,每个子串都和它所有右边的子串进行匹配,这个过程实际上是平方的复杂度,但是由于我们只会去关注超过'Min_Run_Size'长度字串的匹配结果,这就是我们有可能使用哈希表进行优化。
对文本中的每个位置p,我们构造一个下标数组,forward_reference[p]指示下一个长度为Min_Run_Size的hash值相同的run开始的位置,哈希值由hash1()计算得到。如果找不到这样的run,指向的下标就是0。
为了构造这个数组,我们使用一个哈希列表latest_index[],它的长度是一个素数,并且长度与text数组长度相仿。这个哈希列表latest_index[]满足,latest_index[i]是最近出现的哈希值为i的token的位置,如果没有这样的token,值为0。可以查看Make_Forward_References()。
由于哈希的偶然性,上述的构造forward references的方法并不完美。第二遍的扫描(make_forward_references_perfect())通过对Min_Run_Size的tokens做一个完全比较的方法让它们变得完美。(对the LaTeX sources of our book “Modern Compiler Design, 2nd Ed”.)这将总链长度从103555减少到345,这由db_forward_reference_check()测定。db_forward_reference_check()测定数据,检测forward reference。
看完这里的文档,我有一个疑问,文档描述中似乎将token与run进行哈希值的比较,而按照我的理解,run应该是指长度为Min_Run_Size的tokens。我还有一个对“不完美的”理解,就是按照第一遍的哈希结果,应该有很多并不相似的run被判成了相似,所以带着这个疑问往下看。
BTW,在看init_hash_table()的时候,看到TryCalloc函数,百度未果,才发现源码里有Malloc.h,所以他老人家还专门设计了空间管理?这就很厉害了,不过我没有去深究,大概知道它在分配空间就行了。
按照上面所说,扫描实质上是做了两遍的,也就是正好对应:
make_forward_references_using_hash()
make_forward_references_perfect()
第二遍可以理解成是在第一遍基础上的一种“加强”。我们挨个来分析。首先是make_forward_references_using_hash()。
先按照上述所说的,为last_index[]分配好尽量和token数相仿长度的素数长度的空间。
申请完之后,经历一个核心的两重循环后,释放last_index[]的空间。这部分最核心的就是这个中间这个两重循环。可能是学习状态一直不好,看这个两重循环我屡败屡战,每次都问自己,”小问号,你是否有很多朋友?“
外层循环是在遍历所有文件,并在每一次进入内层循环之前把hash值设为0(这个设为0引起了我的注意,因为我本来以为在compare的过程中,token1和token2是否在同一个text里不会影响到他们匹配的,但是hash值却在进入不同的文件之后进行一次初始化操作。这么做的真正用处后面再分析)。在进入内层循环之前,还有两个宏定义,一个是使用宏定义定义了一个函数(眼花缭乱的操作,我做算法题的时候几乎从来不用宏定义撸函数,虽然知道可以这么做),它的作用是以32位规模,进行循环左移。还定义了一个常量SHIFT为5。
进入内层循环,用j作下标来遍历当前文件的token。这层循环比较关键,贴一下代码:
if (
j - txt->tx_start >= Min_Run_Size
) {
int oldest_value =
Token2int(Token_Array[j - Min_Run_Size]);
int oldest_shift =
((Min_Run_Size-1) * SHIFT) % 32; /* oldest_shift这样写,循环过程中都不会有变化吧。 */
hash ^=
Left_Circular_32(
oldest_value, oldest_shift
);
}
/* Circular left shift */
hash = Left_Circular_32(hash, SHIFT);
/* Add new token */
hash ^= Token2int(Token_Array[j]);
if (j - txt->tx_start < (Min_Run_Size - 1)) {
/* no */
continue;
}
size_t run_start = j - (Min_Run_Size - 1);
/* Can the run be useful? */
if (!May_Be_Start_Of_Run(Token_Array[run_start]))
continue; /* no*/
/* the hash value is used here for an index */
size_t h = hash % latest_index_table_size;
if (latest_index[h]) {
forward_reference[latest_index[h]] = run_start;
}
/*latest_index[h] = j;*/
latest_index[h] = run_start;
为了看起来不显得很长,我删了不少注释,需要看作者注释的话可以下载文章开头的源码。说起来这份源码是真的注释丰富,这个习惯也很值得学习啊。这一部分的作用就是进行第一遍hash,按照上述TechnReport文档所说,使用last_index[]为媒介,将具有相同哈希值的tokens串(每个串长度为Min_Run_Size)通过forward_references[]连接起来,现在通过forward_references[],其实可以找到一串串链表,串联所有具有相同哈希值的tokens串,每一串大小为Min_Run_Size。这样就解决了前面看完文档的疑问,当然是相同长度的token串在进行比较啦。
看文档的意思,这样的结果可能会串联过多的串,所以进行第二遍哈希,希望打破一些实际上无意义的串。
第二遍的扫描,就是make_forward_references_perfect()了。这里就遍历了Token_Array[]中的每一个token,对于每一个token的位置i,j初始化成i,然后不断地进行j = forward_references[j],直到i位置和j位置的min_run完全相等,这时候将forward_references[i]赋值为j,这样就直接链接到了j的位置,相当于砍掉了中间一串并不符合要求的链接。直接进行这一遍的扫描而不顾之前的哈希,其实也可以达到“完美化forward_references[]"的目的,但是那样的复杂度就是n方了,而第一遍扫描的复杂度只有n,使用n复杂度的扫描,其实是将哈希值相同作为一个”成功链接的必要条件“,筛掉了很大一部分无意义的匹配,这样来极大地降低了第二遍扫描的成本。
通过这两遍的扫描,Make_Forward_References[]就结束了。
compare_texts()
compare_texts()的总体顺序是:
对于所有的文件
对于所有它必须比较的文本
对于所有在新文件中的位置
对于所有在文本中的位置
对于每一个增长的规模
尽量匹配,并维持最佳
这里有一个新的数据结构,struct range,包括三个数,起始位置、终止位置,第三个sticky暂时没明白。
上来就用n遍历所有文件。下面讨论遍历的过程。
先定义range,然后赋一些默认值。关于代码,这里面其实,像beginning_of_text是可以放到循环体外面的,因为它是Text[0].tx_start,这里可能是为了美观或者方便阅读?range.rg_start = Text[n].tx_start + 1,我不太明白为什么要加一。以及sticky赋值为0,也不是太明白。
接下来三个if就是根据设定的指令来调整了,如果设定了'a'指令,则把range.rg_limit设为Text[n].tx_start(原先是end_of_text,即所有文件的末尾),这样设置的含义就是和全体文件比较,可能是从rg_start开始比较到end_of_text之后,还会返回到开头,继续比较到rg_limit为止。sticky还是不知道啥意思。对于'S'指令,就是只比较old文件了,所以rg_start就是在beginning_of_old_text。设定了's'指令,就是不与自己比较,rg_start等于Text[n].start + 1时,调整为Text[n].limit。不等于呢?那就是设定了'S'的情况,这时候就不改了。
然后检查一下range是不是空的。比如-sS时,n指向最后一个new文件,那么range到这里会被置成rg_start == rg_limit。
若设定'e'需要逐个文件比较,这里就涉及compare_one_on_one()和compare_one_text(),'e'用前者,无'e'用后者。
compare_one_on_one(int n, int m, struct range *rg)
先检测m文件是否在rg内,txt1指向m。构造一个range_m表示m文件的范围。然后再进行compare_one_text(n, &range_m)(所以还是要通过compare_one_text)。
compare_one_text(int n, struct range *rg)
看函数名和参数表,还是容易猜的,这个大概就是要把n号文件和rg范围内的内容进行比较了。不过关注点肯定还是比较具体要怎么进行,如何运用到前面的forward_references[]。
创建txt0指针指向n号文件,i0为txt0->tx_start。完成这两部后,看到了一个宏定义的代码块,叫作#if_def DB_COMP,大概是作者调试用的语句,之后计划在这份源码基础上做一些改进的,到时候可以尝试利用这些指令。当然,记得#if_def要跟#endif。
然后是一个循环,条件为i0 + Min_Run_Size <= txt0 -> tx_limit,按照注释来说,这是“有一个足够的运行空间”的前提。
循环内先判断Token_Array[i0]可以作为运行的开始,用了May_be_Start_Of_Run(),这个函数返回的是!is_in_set(non_initials, tk),is_in_set()在!is_regular_token(tk)的时候返回0,这部分涉及到regular_token的问题,这个是我还没看懂的Token处理的开始部分,就先略过,学一下比较的方法以后再回头研究。
所以如果它满足作为Start_Of_Run的条件,我们还要看看是不是真的存在一个run。先是定义txt_run,i_run,run_size,判定的条件为run_size。run_size = lcs(txt0, i0, rg, &txt_run, &i_run)。lcs,这就有熟悉的味道了。如果run_size大于0,将会add_run(txt0, i0, txt_run, i_run, run_size),i0 += run_size。等于0,那么就只能i0++去看下一个token了。
函数的末尾还有判断rg的sticky值的操作,满足的话,rg->rg_start = i0 - 1,这就相当于rg_start跟着i0一起往前走。这样做的用处,就要分析完lcs()才知道了。
static size_t lcs(struct text *txt0, size_t i0, struct range *rg, struct text **tx_bp, size_t *i_bp)
这是源码里看到现在为止最长的一个函数,突破200行。读着读着有了个心得,叫作注释一定好好看。虽然注释是英文的,但是肯定比代码好懂,毕竟注释是人类的语言,代码是机器的语言。能和一个人交流,就不要选个和一堆代码硬刚。不过以前刷博客的时候,看到过有注释和代码不匹配的情况,这个就另当别论了,出现这种情况分分钟搞崩心态。
这个函数,参数表有注释。
txt0,用来比较的文本。i0,txt0中的起始位置。rg,搜索范围。接下来是两个输出参数,看看是否返回值大于0。tx_bp,输出,best run的文本。i_bp,在best run的文本中的起始位置。
找到最长公共子串(不是最长公共子序列),在如下范围中寻找:从i0位置开始的txt0,对比rg范围中的所有文本。在tx_bp和i_bp中写下位置,并返回size。如果公共子串没有找到,返回0。
检查i0是否在txt的范围中,若不是则fatal。从前面调用lcs的环境来看,是不会出现这种情况的,不过为了保证lcs作为一个模块,自身具有健壮性,这里进行错误处理也是很必要的。这里之后就是调试语句,这一遍看就不分析调试语句了。
来到一个for循环,i1为迭代的量,初始值为first_forward_ref_for(i0, rg)。
first_forward_ref_for()是做啥的呢,它不停地在寻找Forward_Reference(res, i0)。
Forward_Reference(i, i0)又是做啥的呢(就是这样一步步调用下去,相当折腾人。其实也看到过说,人脑能构造栈的深度蛮小的,我这种菜鸟,深度更小了,超过2层就迷糊。但是单独去读每一个文件更加不太能接受,所以还是要硬刚),看到,i为0或者不小于n_forward_references的时候报错,这就是不允许i为forward_references[]的边界。然后new_i即为i的前向链接(为了叙述把forward_references直译了,实际上是从每个点向后串联的)。res作为返回值,主要就是检测new_i是否为0或i0,再赋给res。new_i为i0的情况,res也赋值为0,此时判定为循环。所以i0就是寻找前向链接的终点。总结起来,Forward_Reference就是“寻找i的前向链接,同时判定错误、边界情况”这样的功能。
这样再回来看first_forward_ref_for(),它的功能就是寻找第一个在rg范围内的,i0的前向链接。
这样再回来看这个for循环的迭代过程,就是让i1在rg范围内不断更新为下一个i0的前向链接的值(啊,这样写思路的过程,一层层的函数入栈出栈,脑子真像一个工作栈)。
循环里,首先二分找到i1指向位置所属的文件txt1,检测一下i1和txt1是否匹配,然后就开始寻找better_size。根据size_best是否更新过,赋成size_best + 1,或者Min_Run_Size,这个就是,如果size_best还未赋值,则初始就从Min_Run_Size开始,如果已经赋值了,就把它扩大1,尝试能不能匹配成功更大的长度。
接下来这一块又很关键了(因为它很长)。j0,j1分别是i0,i1在better_size下的右端点。保证j0,j1不超过文件范围,且两段区间不重合的情况下继续尝试匹配。注意不重合不止是j0 < i1,也有可能是j1 < i0,当开了‘a'指令的时候。匹配的时候,区间中开始的Min_Run_Size长度不需要匹配,所以从后往前匹配better_size - Min_Run_Size长度的tokens即可。匹配通过之后,再尝试向后匹配,能不能将长度拉到更长。尝试完了,得到一个new_size,把它交给Best_Run_Size检查一下,可能会删掉一些语言上不接受的长度(评判的标准和细节后面再细看),然后把返回值更新一下,循环就了事了。
所以总结一下,lcs,就是对于txt,i0,找到能够和i0这个位置匹配的最大长度的tokens串,并返回这个长度,利用指针返回相应的位置*i_bp和文件*tx_bp。其实这个模块并不长的,只是充满了注释和调试语句。
这么看来,sticky是在'a'指令下,因为要进行所有文件的比较,所以比较范围需要跟着i0往前走,避免出现自己和自己比的情况。
找到每个位置的最佳匹配之后,就进行add_run()。本来说add_run可以先不看了,结果发现直接往后看Print部分有些云里雾里,明显这里不能够略过了。add_run()是分成两个部分来做的,分别是设定了'p'和未设定'p'的部分。
add_to_percentages(txt0, txt1, size) ---> do_add_to_percentages(0, txt0, txt1, size)
如果设置了指令'p',那么add_run()就会调用add_to_percentages()。
match_list是一个链表的表头,我们看到match的数据结构:
struct match {
struct match *ma_next;
const char *ma_fname0;
const char *ma_fname1;
size_t ma_size; /* # tokens of file 0 found in file 1 */
size_t ma_size0; /* # tokens in file 0 */
};
在do_add_to_percentages()中,用m作为迭代变量,在match_list中寻找txt0和txt1的记录,如果找到了,对应的ma_size += size,然后退出。如果没有找到,就要创建新的节点插入。
创建新的节点就要申请空间。说起来我之前主要是做算法题,基本没考虑空间问题,这里是很细致地考虑空间的。他在空间不够的情况下,先把当前的链表里的都输出,再重新尝试分配一遍。参数里的rec_level就是检测是不是这种情况下的“第二次申请”,如果是,则内存不够退出了,不然一直申请下去一直不行就是死循环。
申请到空间之后,头插法插进match_list就没有问题了。
在分配空间的时候,如果空间不够,处理方法是把当前链表里所有的匹配按照百分比输出。刚开始我考虑这里会不会出现匹配遗漏的情况,比如file_a和file_c比较尚未完成,因为file_a和file_b的新匹配对申请空间而输出。不过其实不会,因为比较是按照文件顺序进行的,如果file_a和file_b进行比较,而链表里已经有file_a和file_c的记录,则说明file_a和file_c已经完成了匹配。也正是利用这一点,在函数末尾判断'u'指令存在的时候,直接输出链表中头结点后面的old匹配对,不会造成遗漏的情况,同时也是old对完成匹配的理想判断条件。
add_to_runs(txt0, i0, txt1, i1, size)
如果没有设置指令'p',add_run()的工作是调用add_to_runs()。
很显然,为了输出百分比和为了输出runs,存储run的时候用到了不一样的数据结构。
struct run { /* a 'run' of coincident tokens */
struct run *rn_next;
struct chunk rn_chunk0; /* chunk in left file */
struct chunk rn_chunk1; /* chunk in right file */
size_t rn_size;
};
( 写这里的时候不知道为什么sublime里面复制出来,空格就会一个顶俩,后来知道了是因为编辑器里把tab作为了indentation,sublime可以在右下角Convert Indentation to Spaces,就可以恢复正常。)
add_to_runs()的思路很简单,按照上面run的数据结构,新建立一个节点r,把两个chunk按照两个txt和两个i分别设定好(set_chunk),然后设定rn_size,最后头插法把r插到runs里作为头节点。那么set_chunk(cnt, txt, start, size)的工作就是需要关注的了。
这时候再来关心chunk的结构:
struct chunk {
/* a chunk of text */
const struct text *ch_text; /* pointer to the file */
struct position ch_first; /* first in chunk */
struct position ch_last; /* last in chunk */
};
所以set_chunk同样地管理好这个结构体里面的值。position怎么管理呢?set_position(pos, type, txt, start)。不用说,这就要关心position的结构了:
struct position {
/* position of first and last token of a chunk */
struct position *ps_next;
int ps_type; /* first = 0, last = 1, for debugging */
size_t ps_tk_cnt; /* in tokens; set by add_run()
in Read_Input_Files() */ /*最后用来输出匹配数量*/
size_t ps_nl_cnt; /* same, in line numbers;set by Retrieve_Runs(),
used by Print_Runs(), to report line numbers
*/ /* 最后用来输出匹配行号 */
};
position指示文件中的一个位置,同时也是一个链表节点,这是每个text都会带有的一个链表,存放一个文件中所有构成chunk的position。根据set_position(),ps_tk_cnt是记录tokens的位置的。所以从每个position中可以读到一个token的位置,也可以按照它的指针读到下一个位置节点,按照set_position()的顺序来看,对于[a, b)这样一个区间,将会先读到b,再读到a,而ps_type为0是左端点,为1是右端点。当然,一个chunk中直接记录了左右端点,所以想要获取这个信息的话并不需要通过链表来搜。
把大脑里深入到position这一层的工作栈弹出position,可以考虑到chunk此时已经记录了匹配上的一个文件的一部分。再弹出chunk,可以考虑到run已经记录了两个匹配的文件各自的始末位置。
Free_Forward_References()
释放forward_reference的指针就可以了。这样,Compare_Files也就结束了,达到的效果就是将每个起始位置,都找到了最匹配的token串,变成run保存起来。而我的疑问就是,构造run的过程中,是保留了最大长度的匹配,那么会不会存在次大长度的匹配仍然是抄袭的,但是没有被检测出来的情况?
后来想了想,没有必要再考虑这个问题,对于当前文本来说,这一段被认定为抄袭,这就够了,不需要知道它抄袭的来源有多少。
Print_Percentages() /* 设定了'p',用百分比输出 */
要开始分析输出模块了,其实输出模块结束,后面就需要回头思考词法分析部分了。
就Print_Percentage()这个函数而言,它首先进行sort_match_list(),然后print_match_list()。排序是为了使得对于同一个文件而言,百分比大的匹配在前面。这个排序,sort_match_list(&math_list),让我看到了从没见过的一个神奇用法。
sort_match_list()
在所有.c,.h文件里,关于sort_match_list()的定义只有这些:
static void sort_match_list(struct match **listhook);
#define SORT_STRUCT match
#define SORT_NAME sort_match_list
#define SORT_BEFORE(p1,p2) (match_percentage(p1) > match_percentage(p2))
#define SORT_NEXT ma_next
#include "sortlist.bdy"
然后排序就能完成了。意念排序?当然不是。这一块的最后一句include了一个.bdy文件,在源码目录找到这个文件,它实现了链表的归并排序。上述宏定义定义了几个名字之后,就可以调用了。这就相当于是一个模板了,神奇。
print_match_list()
在print_match_list()里面,也只有一个只有一行循环体的循环:
while (match_list) {
print_and_remove_perc_info_for_top_file(&match_list);
}
所以其实入口在print_and_remove_per_info_top_file当中。它的功能就是每次输出一个文件的匹配结果。因为链表已经排序了,所以每次先输出头结点。'P'指令的作用是,控制每个文件只输出一个最大的抄袭,所以没有'P'的时候,先输出头结点,然后扫描整张链表,碰到文件名相同的,也输出,并且把这个节点从链表中断开。没有'P',同文件名的就不输出了。在输出每个文件的时候,用了指针的指针传参,理解的时候卡顿了一下,后来发现这样还是很巧妙的。不过作者在注释里表示这段sneaky,我还没有看出来是为啥:
static void
print_and_remove_perc_info_for_top_file(struct match **m_hook) {
struct match *m = *m_hook;
const char *fname = m->ma_fname0;
print_perc_info(m); /* always print main contributor */
*m_hook = m->ma_next;
Free(m);
while ((m = *m_hook)) {
if (m->ma_fname0 == fname) {
/* print subsequent contributors only if not
suppressed by -P
*/
if (!is_set_option('P')) {
print_perc_info(m);
}
/* remove the struct */
*m_hook = m->ma_next; /* 这一步是直接把之前地址链接到下面去了 */
Free(m);
} else {
/* skip the struct */
m_hook = &m->ma_next;
}
}
}
理解这两句话的时候愣了一会儿:
/* remove the struct */
*m_hook = m->ma_next; /* 这一步是直接把之前地址链接到下面去了 */
/* skip the struct */
m_hook = &m->ma_next;
其实是用m_hook不断地追踪下一个节点,而*m_hook就是在修改当前节点的地址了。倘若对这部分有疑问,不妨画个图,第一层是match,第二层是*match,第三层是**match,看看这两句话分别在改变什么东西。
Retrieve_Runs() /* 未设定'p' */
这一块就是在挨个地对每个文件pass2_txt。
在pass2_txt中,先检测文件的tx_pos是否为空,为空则无需扫描。然后打开这个文件。
对txt对应的tx_pos链表进行排序。为什么要排序呢?txt_pos链表是在add_to_runs()模块中,set_position的时候逐个设置的。对于一个text而言,被设置的position有来自“它前面的文件和它产生的匹配”,还有来自“它和它后面的文件产生的匹配”,这样产生的txt_position就是乱序的。用什么方法排序呢?果然,前面的"sortlist.bdy"文件再一次地用上了,把对应的变量名称宏定义一下,就可以直接用了,美丽的代码复用。
match_pos_list_of(txt)。现在我们已经获得了一个排序过的txt_pos链表,现在就是要为每一个pos找到对应的行号。采用的办法是,并行地扫描pos和file,找到合适的行号赋值给pos,再考虑下一个。
这里产生对TK_CNT_HORROR的疑问,在Open_Stream可以看到:
lex_tk_cnt = 0; /* but is raised before the token is delivered,
so effectively *_tk_cnt starts at 1:
TK_CNT_HORROR
*/
并不知晓为什么这个计数会在token给出之前增加,等回头看分析词法的时候再去考虑。
经过取回runs的操作,pos和行号就对应上了。
Print_Runs()
这块是真的凶,又是一个三百多行的.c文件。看pass3.c的文件,开头部分的注释讲到编码格式、字符粒度等等看起来非常陌生的东西,极大地阻碍了我读下去的信心和积极性,其实是为了尽量保证输出的格式。(看pass3.c文件的时候,发现按函数的定义顺序读可能更加容易一些,之前那样递归地读下去,一会儿就碰到新的陌生的模块,而顺序读,读着读着发现他用到的都是之前读过的模块。当然,这还是在源码严格按照“先定义后使用”的顺序写的前提下的。)
稍微看了一些,暂时不全部看细了。今天请教大佬学姐,她表示搞这些东西不能看的太细,研究和要改的部分的有关的内容,其他地方大概了解用途就好了,我之前有些偏执了,所以掉过头来抓紧看词法。词法就发在下一篇吧。