存档: 2010 July
最好用的调试方法肯定是printf了:-),比如说,我们可以这么打印z的值:
z = x + y;
printf ("z = %d\n", z);
然而,我们常常只对printf的部分感兴趣,因此,我们可以使用#ifdef宏
z = x + y;
# ifdef DEBUG_MODULE_A
printf ("z = %d\n", z);
# endif
但这又引出了一个问题,每次都要写#ifdef未免有点cumbersome了.为了使代码轻松一点,我们可以这么写:
z = x + y;
debug_module_a ("z = %d\n", z);
或者:
debug ( module_a , "z = %d\n", z);
写的规范点,代码就是这个样子了,
z = x + y;
DBG(("z = %d\n", z));
DBG的定义为:
# ifdef PRGDEBUG # define DBG (x) printf x # else # define DBG (x) /* nothing */ # endif
其实,上面的做法都是很常见的.但在添加模块后,调试宏应该怎么写就确实能够区分出做好的好坏了.
首先,为了简洁,我们当然还是希望打印状态时可以这么写的,
DBG (( MOD_PARSER , "z = %d\n", z ));
这里的模块编号MOD_PARSER也可以是MOD_SOLVER或者是M0D_PRINTER,因为多了这么一个参数,printf的方法便失效了,所以,我们还需要额外定义一个状态输出函数:
void dbg_printer ( int module , const char *fmt , ...) {}
宏的定义为:
# ifdef PRGDEBUG # define DBG (x) dbg_printer x # else # define DBG (x) /* nothing */ # endif
使用如下:
z = x + y; DBG (( MOD_PARSER , "z = %d\n", z ));
我们知道,既然需要动态地添加模块,那么程序往往是会提供一个配置文件的,比如说一个名为”debug_modules.conf”的文件,内容如下:
ADD_MOD (0, PARSER ) ADD_MOD (1, SOLVER ) ADD_MOD (2, PRINTER ) //后面可能是一些不必使用的模块配置 UNNEEDED_MOD(3, PREPROCESSOR)
它表示程序将加载的模块.这时又引出了一个问题:在调试相关的代码中,我们需要提供一个函数读配置文件和所需模块所在的行号,这时代码也许就变成了:
# define DBG (x) dbg_set_pos ( __FILE__ , __LINE__ ), \
dbg_printer x
static const char * file_name ;
static int line_num ;
void dbg_printer ( int module , const char *fmt , ...) { ... }
void dbg_set_pos ( const char * file_name , int line_num ) { ... }
这时代码就变得相当难看了,怎么才能优雅起来呢?一个不错的方法是,使用宏连接:定义一个头文件”debug.h”,内容为:
...
# define ADD_MOD (num , id) MOD_ ## id = 1 << num ,
enum _debug_modules_t {
# include "debug_modules.conf"
};
# undef ADD_MOD
...
在调试代码中,直接包含该头文件即可,头文件内容将被预处理器自动转换为:
enum _debug_modules_t {
MOD_PARSER = 1 << 0,
MOD_SOLVER = 1 << 1,
MOD_PRINTER = 1 << 2,
};
终于开始实习了,小组组长是很好人的一面面试官,导师是好安静的小白..需要开始努力了.带了几本书到深圳,不知道有没有时间看,虽然有些是看过了的.
- APUE
- UNP v1~v2
- TCP/IP v1
- CLRS
- CSAPP
- Expert C programming
- Design Patterns
- More Effective C++
- Inside The C++ Object Model
- Understanding The Linux Kernel
- Linux 内核完全剖析
- 程序员的自我修养
- 编程之美
以前总以为 ,跟着牛人一起学习,耳濡目染肯定进步快些.最近总算有点明白了,其实牛人并没有那么多的时间照顾菜鸟;再者,牛人也是从翻了无数经典的菜鸟成长起来的.既然有如此多的大神,愿意慷慨地将自己的时间花在著书立说之上,用深刻,全面,系统的知识指引着我们,那么,多看好书,成长的速度不是应该更快一些吗..这总是没错的.
P.S. 今天狠狠地蹭了组长一顿饭.
P.S.2 电脑前一天坏了,酒店网速又好慢..洗洗睡吧.
资源需要分配,同样也是需要销毁的.<程序员修炼之道>中原则35,”finish what you start”说的便是这个道理..
下面是一个很糟糕的程序,它打开文件,读取信息,更新字段,然后写回结果.’
FILE *fd;
void readObject(const char *filePath, Object *obj)
{
fd = fopen(filePath, "r+");
fread(obj, sizeof(*obj), 1, fd);
}
void WriteObject(Object *obj)
{
rewind(fd);
fwrite(obj, sizeof(*obj), 1, fd);
fclose(fd);
}
void updateObject(const char *filePath, int weight)
{
Object obj;
readObject(filePath, &obj);
obj.weight = weight;
writeObject(&obj);
}
乍看起来,程序可以工作得很好.但是,因为共享fd,readObject和writeObject却紧紧的耦合在一起了.可以很轻松的举一个例子,证明两个函数耦合带来的问题.比如说,要求仅在weight大于100时才做更新,那么updateObject函数将被修改为,
void updateObject(const char *filePath, int weight)
{
Object obj;
readObject(filePath, &obj);
if(weight > 100)
{
obj.weight = weight;
writeObject(&obj);
}
}
这就引入了一个问题,WriteObject在某些情况下并不会被调用,以致于文件未被关闭,这将浪费系统的资源.
一种解决方案可以是
void updateObject(const char *filePath, int weight)
{
Object obj;
readObject(filePath, &obj);
if(weight > 100)
{
obj.weight = weight;
writeObject(&obj);
}
else fclose(fd);
}
这看起来貌似不错,不管新的weight是多少,文件总是会被关闭..但其实,这是一个非常糟糕的修改,readObject,writeObject和updateObject三个函数由于共享fd耦合在一起了.简单的web服务器中的代码就是如此,爬出一个陷阱,却掉进了一个更大的坑,这不得不说是种悲剧.
要解决这个问题,可以遵守一个原则,资源的分配和销毁这一责任由同一个函数承担,打开和关闭在同一地方,而且配对出现.重构如下:
void readObject(FILE *fd, Object *obj)
{
fread(obj, sizeof(*obj), 1, fd);
}
void WriteObject(FILE *fd, Object *obj)
{
rewind(fd);
fwrite(obj, sizeof(*obj), 1, fd);
}
void updateObject(const char *filePath, int weight)
{
FILE *fd;
Object obj;
fd = fopen(filePath, "r+");
readObject(fd, &obj);
if(weight > 100)
{
obj.weight = weight;
writeObject(fd, &obj);
}
fclose(fd);
}
这样做还有一个附加的好处–去掉丑陋的全局变量FILE *fd.
异常下的资源分配和释放
在使用异常时,很容易写出这样的代码,
void doit()
{
Node *n = new Node;
try
{
// do something
}
catch(...)
{
delete n;
throw;
}
delete n;
}
注意到,n是在两个地方被释放的,一次是在程序正常路径上,一次是在异常处理中.这显然是违反DRY(Dont repeat yourself)原则的,也许会像前面一样导致维护问题.
解决上面的问题,其实可以用到3种小技巧.一是,利用C++的性质–局部变量在作用域外会被自动销毁.如果允许的话,我们可以将’n'从指针变为栈上的实际的对象.
void doit1()
{
Node n;
try
{
// do something
}
catch(...)
{
throw;
}
}
这样,不管是否抛出异常,C++都将自动析构Node对象.
当然,由于各种原因,用对象替代指针的做法可能是不被允许的.变通方法很简单,我们可以将指针包装在另外一个类中,
class NodePointer
{
Node *n;
public:
NodePointer() { n = new Node; }
~NodePointer() {delete n; }
};
void doit2()
{
NodePointer n;
try
{
// do something
}
catch(...)
{
throw;
}
}
最后一种方法是直接利用C++库中的模板类auto_ptr,它将自动包装动态分配的对象.
void doit3()
{
auto_ptr<Node> p (new Node); // 用p->访问.
try
{
// do something
}
catch(...)
{
throw;
}
}
