标签 C 下的文章

追求'lint-clean'

到底需不需要编译器之外的独立的静态代码检查工具呢?这个问题’仁者见仁,智者见智’。但是有一个结论我想大家都会认可,那就是越是在开发周期早期发现的Bug,修复它所付出的代价就越小。而像lint这样的静态代码检查程序恰恰是让Bug在早期阶段’显露原型’的绝佳工具,而追求’lint-clean’[注1]境界的代码也向来是专家级程序员的嗜好。别忘了在’C专家编程’一书中曾经提到Sun OS的内核一直是保持’lint-clean’状态的,这就是榜样!还等什么?赶快学呀!^_^

有人抱怨’不敢用lint工具, 太多的Warnings把快屏幕都淹没了!’,不过高手一般不这么想,他会细心琢磨这些Warnings背后的’暗示’,并和lint工具沟通,利用lint工具提供的交互方法屏蔽掉一些经过分析认为不能成为错误的Warnings。久而久之,高手本身就成了一个lint程序,就能够很快的用肉眼发现代码中的问题,并指出问题所在,如何解决!他还能告知如何嵌入一些Annotations从而避免让lint程序产生不必要的Warnings,这时这位高手对语言和程序的理解就又提高了一个档次了。其实使用ling工具不仅仅是为了提早发现程序中的Bug,其使用过程有助于你加深对程序的认识和理解。的确事实就是这样。

Splint就是一款强大而且应用广泛的开源lint工具。它的强大的代码检查能力固然让人称道,但是让我更欣赏的却是它提供的’Annotations’机制。Splint可以让程序员在自己的代码中嵌入相应的Anotations,这些Anotations作为Splint分析代码时的输入以帮助Splint产生对程序员更有用的信息。下面是一些Splint的使用入门,更多详细信息请查看’Splint manual‘。

1、最简单的Splint使用方法
>> splint *.c

2、Splint输出Warnings的基本格式
<file>:<line>[,<column>]: message
     [hint]
      <file>:<line>,<column>: extra location information, if appropriate
我们可以使用’+/-<flags>’来自定义其输出格式,如’splint -showcol *c’,则Splint不会在输出信息中显示’列’信息。

3、使用flags控制splint的检查范围和输出格式
‘+<flag>’ — 表明某个flag处于打开状态,如’+unixlib’;
‘-<flag>’ — 表明某个flag处于关闭状态,如’-weak’;

4、使用.splintrc环境文件
如果不想每次使用splint的时候都手工输入一堆’+/-<flags>’,那么你可以把这些’+/-<flags>’预先写到.splintrc文件中,当splint执行的时候它会自动加上这些flags的。默认的flags设置在’~/splintrc’文件中,但是如果一旦splint的当前工作路径下也有.splintrc文件,那么这个.splintrc文件中的flag设置会覆盖’~/splintrc’中的flags设置,但是命令行中的flags设置是具备最高优先级的,它会覆盖前面提到的任何一个文件中的flags设置。

5、使用Annotations
对于’Annotations’的作用,Java程序员并不陌生,但是C程序员则对这个不是那么了解。C代码中的Annotations用来指导Splint生成恰当的代码检查报告。下面这个例子对比使用和不使用Annotations,Splint的输出的差别:
/* testlint.c */
void foo1() {
        /*@unused@*/int *p = NULL;
}

void foo2() {
        int *p = NULL;
}

splint testlint.c
Splint 3.1.1 — 28 Apr 2003

testlint.c: (in function foo2)
testlint.c:6:7: Variable p declared but not used
  A variable is declared but never used. Use /*@unused@*/ in front of
  declaration to suppress message. (Use -varuse to inhibit warning)

Finished checking — 1 code warning

可以看出没使用Annotation的函数foo2被给出Warning了。Splint的Annotations繁多,我们在平时做lint时可以多多接触。

‘早用lint,勤用lint’,这是C专家给我们的建议。’lint-clean’也许离你并不遥远。

[注1]
‘lint-clean’ — 程序能够顺利通过lint程序的检查。

C语言也重构

所谓的重构是这样的一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法,可以将整理过程中不小心引入错误的机率降到最低,本质上说,重构就是[在代码写好之后改进它的设计]。Martin Fowler

重构,一种改善代码’体质’的方法’。侯捷

重构是上进程序员每天的进行式。是一项工程而不是靠着天份挥洒的艺术。– 侯捷

如果有人说’重构’仅仅限于Java语言或者其他面向对象的语言,那他就大错特错了。按照Martin Fowler的诠释,’重构’就是一种代码整理,而且是在保持接口功能不变的一种代码整理,这样的话每种语言都可以’重构’,也许’重构’在你平时的工作学习中已经不知不觉地发生了,只是你还没有认识到那是’重构’罢了。自从从大学毕业后我就一直在’把弄’C语言,对C语言也算是’情有独钟’了,所以自然而然的就开始探索和积累C语言的重构方法。这里个人能力毕竟有限,并不能穷尽所有方法。

按照我的理解,重构[注1]应该是’三位一体’的,即"前提(或叫保证) + 理论 + 工具",前提(或保证)指的是单元测试,回顾一下本篇首Martin Fowler对重构的诠释 — ‘在不改变代码行为的前提下’,而如何保证代码经过’大修’后,能保证其外在行为不变呢,只有测试;理论则是指重构的来龙去脉、理论和方法了,这也是本篇所主要关注的;至于工具,那就是重构’催化剂’,保证对大型项目重构的效率,遗憾的是至今业界仍没有一款成熟的支持C语言的重构工具,而与此相反针对像Java语言这样的面向对象语言,各厂家则是’众星捧月’,工具也不断推陈出新,其中缘由也不难理解。

Internet上讨论C重构的资料少之又少,个人认为比较全面的是Alejandra Garrido的’CRefactory project‘,说这份资料比较全面是因为Alejandra Garrido将之分类清楚(但也许不是所有人都喜欢这样分类的,我就是其中之一),但是这份资料不够详细,关于每条item的说明仅仅几行,而且没有例子。不过不管怎样,这份资料都是值得看的,它也许会给你提供一些思路上的提示。

关于重构理论最权威的莫过于Martin Fowler那本’Refactoring – Improving the Design of Existing Code’了,虽然Martin青睐Java,但这并不太多影响其他’语种’的小工们从这本大师的书中汲取’思想的精华’。

在这里我仅针对C代码中的’Bad Smell’给出自己的重构想法,之所以没有像Martin书中那样给出分类,也许是因为自己对重构的理解还不那么到位的缘故^_^,也许以后会给出像模像样、合情合理的分类,好了,下面我们就一起开始嗅嗅代码的味道吧。

1、Duplicated Code
重复代码自古有之,历史悠久自不必说了。Martin书中将之放在Bad Smell的第一位,足见其’Bad Enough’^_^。’Duplicated Code’将给你带来诸多不便,诸如维护更多的代码、代码修正后可能的不一致性等,这里列举三种情况来具体说明How smelly the ‘Duplicated Code’ is!

(1) 太多的128、256…
[e.g.]
void func1(…) {
 char path[128];
 …
}

void func2(…) {
 char path[256];
 …
}

[solution & notice]
#define  MAX_PATH 256

void func1(…) {
 char path[MAX_PATH];
 …
}

void func2(…) {
 char path[MAX_PATH];
 …
}

避免在程序中多次出现magic number,特别是当多个magic number重复时,坏的味道就散发出来了,我们可以定义一些常量符号还替换这些magic number。

(2) 一山难容二虎
[e.g.1]
void func1() {
        time_t          in_tm;
        time_t          out_tm;
        char            in_str[100];
        char            out_str[100];
        struct tm       *ptm = NULL;

        memset(in_str, 0, sizeof(in_str));
        memset(out_str, 0, sizeof(out_str));

        in_tm   = time(NULL);
        out_tm  = time(NULL);

        ptm = localtime((time_t*)&(in_tm));
        sprintf(in_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);

        ptm = localtime((time_t*)&(out_tm));
        sprintf(out_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

[e.g.2]
//in test.c
void func1() {
        time_t          in_tm;
        char            in_str[100];
        struct tm       *ptm = NULL;

        memset(in_str, 0, sizeof(in_str));

        in_tm   = time(NULL);

        ptm = localtime((time_t*)&(in_tm));
        sprintf(in_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

// in test.c, too
void func2() {
        time_t          out_tm;
        char            out_str[100];
        struct tm       *ptm = NULL;

        memset(out_str, 0, sizeof(out_str));

        out_tm   = time(NULL);

        ptm = localtime((time_t*)&(out_tm));
        sprintf(out_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

[solution & notice]
在一个函数中存在功能相同的’代码群落’(如e.g.1)或者是在同一个源文件中在不同的函数实现中存在功能相同的’代码群落’(如e.g.2),这是典型的’Duplicated Code’,解决方法很简单 — 析出函数(Extract Function)或者析出宏(Extract Macro, 这可是C语言的特色哟)。如解决上面两个例子中的问题我们便可以这么做:
a) 析出函数time2str
void time2str(char *s, const time_t t) {
        time_t          tmp     = t;
        struct tm       *ptm    = NULL;

        ptm = localtime((time_t*)&(tmp));

        sprintf(s, "%d%02d%02d%02d%02d%02d",
     
           ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

b) 析出宏TIME_2_STR
#define TIME_2_STR(s, t)     do { \
                struct tm *ptm = localtime((time_t*)&t); \
                sprintf(s, "%d%02d%02d%02d%02d%02d", \
                        ptm->tm_year+1900, \
                        ptm->tm_mon +1, \
                        ptm->tm_mday, \
                        ptm->tm_hour, \
                        ptm->tm_min,  \
                        ptm->tm_sec); \
        } while(0)

用time2str或TIME_2_STR替换掉那些重复的代码后,就会发现原来世界可以这么简洁!

(3) 你中有我,我中有你
[e.g.]
/* foo1.c in project foo */
static int foo1_mkdir(const char *path) {
 …
}

/* foo2.c in project foo */
static int foo2_mkdir(const char *path) {
 …
}

[solution & notice]
/* foo_common.c */
static int foo_mkdir(const char *path) {
 …
}

/* foo1.c */
#include "foo_common.h"

/* foo2.c */
#include "foo_common.h"

在较大的工程中,多个程序员并行工作,不可避免的在各自的代码中实现了相同功能的一些方法,这就是’Bad Smell’,这样程序员要维护两份实现相同功能的代码,而且当fix bug时很有可能漏掉更新另一份。这种的散落在不同源文件中重复代码可以采用’Pull Up Method’方法将这个功能接口提取到一个公共的文件中去实现,而原先的两个文件只需包含接口头文件即可。

2、Long Function/Procedure/Method
事实证明越长的Function,理解起来越难,维护难度也越大。拆分函数意味着要把代码中一个功能相对独立的’代码群落’独立出去,这样其实带来了很多好处。首先你又给了自己一次重新阐述代码意图的机会,你可以起一个恰当的名字来表述这段被独立出去的’代码群落’的’工作性质’;其次一旦这段’代码群落’被独立出去了,它就可以被其他函数逻辑所共享;再者独立出去的’代码群落’与原先的代码的关系照比之前已经不那么紧密,所以一旦’代码群落’里发生什么’叛乱’,其不会蔓延到原先的代码中。在Martin书中曾专门引用Kent Beck的一段关于’Indirection and Refactoring’的论述来说明’拆分’的利弊,而在整个重构的理论中’拆分’的确也占据着’主流’地位。这里就不举例说明了,拆分也是我们平时经常做的事情,不是吗!:)

3、Long Parameter List
长长的参数列表虽然不是致命的,但却是最最不顺眼的。想想当你去调用一个接口时发现它有10个参数,为完成这个调用你需要来来回回看N遍那个接口的原型还不见得传入的实参全部符合原型要求。实际情况就是这样,而且遭遇这种情况的时候还是很多的。怎么办?重构!如何做?看下面例子。

[e.g]
int x_add_book(const char *title,
                const char *author,
                const char *isbn,
                const char *pub_date,
                int pages,
                int price,
                const char *publisher,
                const char *pub_addr,
                const char *pub_homepage);

[solution & notice]
哇,添加一本书要这么参数亚,晕倒!让它少些参数吧 — 合并!这个例子中的参数之间有相关性,可以尝试组合一下。组合完可能是这样的:
struct pub_info {
        char            name[MAX_NAME_LEN];
        char            addr[MAX_STR_LEN];
        char            homepage[MAX_STR_LEN];
};

struct book_info {
        char            title[MAX_NAME_LEN];
        char            author[MAX_NAME_LEN];
        char            isbn[MAX_STR_LEN];
        char            pub_date[MAX_STR_LEN];
        int             pages;
        int             price;
        struct pub_info pub;
};

int x_add_book(const struct book_info *book);
重构完了。不知道大家发现这样修改的好处没有,比如现在我想在添加一本书时,再加上这本书的位置信息,如放在那个书架上,这样如果我们使用e.g.中的做法,我们需要修改接口;而使用重构之后的方式我们只需要修改book_info结构体,x_add_book接口则无需修改!这也是增加一个间接层的好处之一!

那么是不是所有的长参数列表的函数都可以这么重构呢?有些时候参数列表中的参数都是毫无联系的,如果硬要把他们’组合’在一起反倒不顺眼,这也符合’强扭的瓜不甜’的道理:) 这时候可以考虑某个参数是否可以调用其他函数得到该值,如果可以的话,我们就可以在删除该参数而改在函数体中调用其他接口获取这个值,这种方法类似于Martin书中的’Replace Parameter with Method’;尽量不要用全局变量来代替某一参数,过多的全局变量也是Bad Smell!

如果还不行的话,没办法,保留这一长参数列表的接口,千万不要为了’哨子’而付出太多的代价!

4、Comments
我们不是反对注释,注释是很好的增强代码可读性和可维护性的手段,而且注释有时会帮助你发现代码中的味道!想象一下我们的注释一般会出现在什么样的位置呢?如下图:
/*
 * 注释1
 */
return_type func_name(params list…) {
 /*
  * 注释2
  */
 …

 /*
  * 注释3
  */
 …’代码群落’…
 
}

一般我们在’注释1′的位置描述函数的功能,有时候我们发现这段注释描述比函数名更加清楚,这也是一个不好的味道,这时我们可以使用’Rename Method’来重构之;在注释2的位置我们可能会描述一些’precondition’,这里不妨用’Introduce Assertion’来替换掉这些注释;注释3会详尽介绍’代码群落’干的是什么活,仔细考量,看看能否将这段’代码群落’用’Extract Method’方法析出,而这段注释恰恰可以为析出后的函数作命名之用。另外’Rename Method’和’Introduce Assertion’两种重构技巧较简单,望文即可生义,自然无需多说!

5、其他
Alejandra Garrido的’CRefactory project’中的一些条目还是对我很有启发性的,这里仅列出我认为必要的。
(1) contract variable scope
我将之理解为:’约束变量的作用范围’:一个在特定作用范围内定义的变量仅仅被该范围内的一个inner scope中的代码所使用,我们大可把这个变量声明移到这个inner scope中。

(2) Replace expression with variable
这其实也是个’Duplicated Code’问题,只是重复的’代码群落’是一个expression,如 (i < 10) ? 1 : 0条件表达式等,这样的话我们把它赋值给一个变量,然后在以后使用这个变量即可。

(3) Convert global variable into parameter
对于这个条目我们可以举个例子:
[e.g]
int g_cnt = 1;

void fun1() {
 if (g_cnt > 10) {
  g_cnt = 1;
 } else {
  g_cnt++;
 }
}

void func2() { /> g_cnt += 3;
}

void func() {
 if (g_cnt == 2) {
  func1();
 } else {
  func2();
 }
}

[solution & notice]
在这个例子中func、func1和func2都对全局变量g_cnt进行了访问,并且都有权对g_cnt进行修改,而实际func1和func2的设计意图可能就是func的辅助函数,负责对func需要操作的变量进行修改,而不一定都需要访问全局变量,这时我们可以这样修改:
int g_cnt = 1;

void fun1(int *cnt) {
 if (*cnt > 10) {
  (*cnt) = 1;
 } else {
  (*cnt)++;
 }
}

void func2(int *cnt) {
 (*cnt) += 3;
}

void func(int *cnt) {
 if (*cnt == 2) {
  func1(cnt);
 } else {
  func2(cnt);
 }
}

func(&g_cnt);

感觉这个条目的目的就是尽量减少有权访问全局变量的访问点个数。

OK! C语言并不复杂,所以重构的技巧、代码的smell也不如面向对象语言那么多,呵呵,这里我就想到这么多了!最后别忘了重构仅仅是一种有计划有规则的’代码整理’,它并不是那种不可逾越的技术高峰。在积累了一段时间的重构’经验’后,重构完全可能会成为你开发过程中的一个不可缺少的习惯了,而养成习惯的必经之路就是持续不断的练习、积累和总结!让自己从此走上’C语言重构之路’吧。

[注1]
关于重构重要性、重构时机等话题在Martin书中有详尽说明!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



Statcounter View My Stats