第9篇 #define的使用与代码运行检测方法

中山野鬼 发布于 2012/11/27 16:59
阅读 2K+
收藏 42

    我们前面使用了宏定义,实现了如下的一个打印
    #define __PRINT_FUNC() do {printf("%s func!\n",__func__);}while(0)
    这里说句鬼话:
C语言要写的好,至少有三个门槛要过,指针,模块化编程思想,宏及预处理。
学好宏,包括两个方面内容,一个是宏的处理方法。一个是已有的系统宏的利用。
    这里有个不严谨的说法,诸如__func__是预定义,而诸如__lines__是宏。你可以在参考文献1的index中寻找到正确答案。
    先说说运行。如果使用过VC的朋友,应该知道几个功能键。
    F9,设置一个断点
    F5,运行,指导断点处停下来。
    F10,仅运行一条语句。
    F11,和F10很像,但如果该语句是个函数调用,会直接进入该函数,而不是让这个函数运行完毕。
    而对应linux下,GNU也提供了强大的GDB。以上两种方式均有一个共通特点,就是依赖代码编译生成的辅助信息,进行阻塞式测试。我们也可以称为同步测试。与之对应的是异步测试。
鬼话:同步测试,算我自创的名词了,如有雷同,含义不保证一致。这里说的同步测试,即观测者对代码的测试,依赖于代码运行到指定测试逻辑的测试点,观测程序各个资源状况。代码运行,和观测处于同步。而对应的异步测试,则是程序持续进行,而观测根据输出信息自行分析,两者没有必然的同步点的问题。再简单说,同步测试,如同盲人走路,走一下,用导路棒测一下,而异步测试,如示波管对一个信号进行测量,挂上去就有,不挂也不会导致信号关闭,当然信号源不工作时,挂不挂都没有。
    同步测试,和异步测试,都有什么好处?对比对比?我的态度,前者简单,后者烦;前者直接,后者混乱。
    同步测试,通过gcc的编译-g的参数,就可以获得足够的调试信息,并通过这些调试信息帮助你进行调试,而后者,本质上是需要你添加打印代码,将对应要观测数据,输出到外部。
    同步测试,你可以方便的观测到当前各种对应变量,甚至寄存器的情况,包括内存的值。而后者,就是一堆堆的输出,如同log一样。
    既然传统方式这么好,为什么还要使用异步测试?我先举个例子
static int tmp = 0;
int filter(int *p,int n){
    int t = 0;
    while (n > 0){
        t += (*p++ - tmp);
        n--;
    }
    tmp +=  (t / n);
    return tmp;
}
    我们先不要考虑上面这个函数的目的是什么。我们看下,如何测试这个函数的溢出。
    1、*p++ - tmp 这个是会溢出的。 但如果它的结果不溢出,也不代表 t不会溢出。
    2、t不溢出,不代表tmp不会溢出。
    同步测试,确实可以逐步在while循环里进行检测。但假设,n = 1024,而溢出点在1021的位置,看似简单的同步测试,仍然会变的超级麻烦。与其这样,不如老实的加上打印信息,输出后,通过对文本分析的程序,依次进行检测,或直接对打印输出做判断检测。
    
int testA(int *p){
    ....
}    
int testAA(int *p){
       .....
}
int testB(int *p){
        ...
        for (i = 0 ; i < 1000; i++){
            testA(p+i);
            if (i & 0x3 == 0){
                testAA(p+i+i);
            }
        }
        ....
}
int testC(int **pp){
    ....
    for (j = 0 ; j < 1000; j++){
        testB(pp[j]);
    }
    ...
}    
    如果在testA中,存在一个错误,而其发生在testB中 i = 584,testC中,j= 637 的时候。聪明的做法,是每次调用testA,我们额外增加一个计数,根据这个计数,决定断点的位置。例如
static int n;
int testA(int *p){
    if (n == (637 * 1000 + 584)){
        n++;
    }else{
        n++;
    }
    .....
}
这样我们就可以通过同步测试,找到错误点。但可能存在一个衍生问题,我们发现 此时,testA并没有错。而是对应此时testAA错了。于是,你需要再次计算,此时testAA运行了多少次,例如
#define I_BREAK 584
#define J_BREAK 637
#define A_BREAK (I_BREAK + J_BREAK * 100)
#define AA_BREAK ((I_BREAK >> 2) + J_BREAK*100)
static int n;
int testA(int *p){
    if (n == A_BREAK){
        n++;
    }else{
        n++;
    }
    .....
}
int testAA(int *p){
    if (n == AA_BREAK){
        n++;
    }else{
        n++;
    }
}
    于是你又可以继续了。貌似同步测试没有任何困难。恩,确实,不过这是基于你知道584,和637这两个值。也就是说你准确的知道什么时候 了。这里提出两个名词,一个是找哪错,一个是找错哪?我的解释,找哪错,针对代码,准确的将逻辑不合理的语句锁定。而找错哪,针对存储空间的值,哪个值错了。两者在代码debug中,经常交替出现。例如:先发现一个错误的数据,于是去回头找,哪的逻辑将其改写的,定位后,看逻辑是否有错,通常非新手,逻辑没错,但是是数据错了。于是,又开始寻找是哪个相关数据错了。
    584,637这种值是可以用同步方式,手工测试出来的吗?有些情况下,未必。例如,当一个错误不是由自身代码逻辑导致的,而是由存储空间使用混乱导致,这会因为,存储空间的数值的改变,而导致错误的情况不同,而不是傻乎乎的,程序错的每次都一样。
    因此,同步测试的方法确实简单好用,但更适合静态逻辑的测试,例如测试数据一致,代码不变,存储空间不被其他外部程序干扰。而异步测试    可以看作是状态记录型测试。通过对输出的数据,进行分析,以获取相关数据的变化因果关系,最终锁定问题。
    这里不再举例采用异步测试更为有效的案例。因为这涉及太多的代码逻辑解释。也很难做运行验证。我们直接开始讨论异步测试怎么设计。
    先考虑下,我们测试,需要哪些动作。
    打印出是那个C文件。
    打印出函数名是肯定要的。
    打印出哪一行也是肯定要的。
    还要能灵活打印出各种储存空间的内容,寄存器就算,此处指有实际存储空间名的那么你定义它为寄存器存储类型。
    整体辅助打印代码能尽可能的简单方便。
    能整体屏蔽掉,以获得干净的代码。
    函数名,简单我们已经实现了。参考文献1 6.10.8.1中的 __FILE__ ,__LINE__ ,我们可以利用它实现打印当前文件,当前行数。
    现在修正 define_attr.h如下
#define __PRINT_POS() do {fprintf(stderr,"%s(%d){%s}",__FILE__,__LINE__,__func__);}while(0)    
#define __PRINT_FUNC()  __PRINT_POS()  //do {printf("%s func!\n",__func__);}while(0)
    保存,编译运行。看什么结果。
    会有诸如这样的输出
    src/control.c(143){check_grammar}
    当然你可以自己调节__PRINT_POS()的操作。
    现在我们需要打印出一个存储空间的内容。传统方法,假设打印一个整型,很简单
    printf("%s,%d","i",i);
    我们看一下新的debug_attr.h的清单。
#ifndef _DEFINE_ATTR_H_
#define _DEFINE_ATTR_H_
#include <stdio.h>
#define __PRINT_POS() do {fprintf(stderr,"%s(%d){%s}\n",__FILE__,__LINE__,__func__);}while(0)    
#define __PRINT_FUNC()  __PRINT_POS()  //do {printf("%s func!\n",__func__);}while(0)

#define mkstr(a) # a
#define __UNION_EXP2STR(c,d) mkstr(# c  d)
#define __VARLOG(exp) do {printf(__UNION_EXP2STR(exp, = %d),exp);printf("\n");}while (0)

#endif
    我们在value.c的init_all中增加一个内容    
int init_all(void){
    if (init_flag == 1) {return init_flag;}
    init_flag = 1;
    init_flag = init_flag & init_status();
    init_flag = init_flag & init_model();
    init_flag = init_flag & init_control();
    init_flag = init_flag & init_view();
    init_flag = init_flag & init_MVbuf();
    __VARLOG(init_flag);
    return init_flag;
}
    编译,这里会有错误提示,自己找方法去除,然后链接,运行。你看是否有
    init_flag = 1
    的打印信息。
    恩。很神奇,没错,C语言虽然古老,但是很全面,几乎为你准备好了很多事情。无非你好好学习利用。现在分析一下为什么可以打印出
    init_flag =1 ,
    __VARLOG(init_flag);这等同于
    printf("init_flag = %d",init_flag); printf("\n");
    printf(__UNION_EXP2STR(exp, = %d),exp); exp可以理解。实际是 init_flag,我们关注一下
    __UNION_EXP2STR(exp, = %d)
    其等于
    mkstr(init_flag = %d)
    等于
    #init_flag = %d
    等于
    "init_flag = %d"
    你可以参考文献1 6.10.3 这个整体章节均值得你阅读。# a 的操作,注意中间有空格。可以在预编译时,自动转换为"a"。将一个文本上
    体现的字符串,转换为,C语言里的字符串。你可以尝试如下处理
#ifndef _DEFINE_ATTR_H_
#define _DEFINE_ATTR_H_
#include <stdio.h>
#define __PRINT_POS() do {fprintf(stderr,"%s(%d){%s}\n",__FILE__,__LINE__,__func__);}while(0)    
#define __PRINT_FUNC()  __PRINT_POS()  //do {printf("%s func!\n",__func__);}while(0)

#define mkstr(a) # a
#define __UNION_EXP2STR(c,d) mkstr(# c  d)
#define __VARLOG(exp) do {printf(__UNION_EXP2STR(exp, = %d),exp);printf("\n");}while (0)

#endif
    此时,则打印的是 "init_flag" = 1 ,而不是 init_flag,这等同于
    printf("\"init_flag\" = %d",init_flag);
    但此处多了\是因为你文本书写C代码时,如果需要让一个字符串保留"这个符号,则需要用\来明确。而实际存储空间里的内容仅
    有"init_flag" 这11个字符,外加最后一个0。
    由于 # init_flag 这个是发生在预编译处理过程中,所以你不许要考虑文本描述中才使用的\的方式。
    你是否很鬼的想到,我们玩一玩 ""init_flag""的打印?于是你有了
    #define __UNION_EXP2STR(c,d) mkstr(# # c  d)
    不好意思,第一个 #就直接把 #当参数了。但是这可不允许。于是会提出错误。或许你又想到如下做法
    #define __UNION_EXP2STR(c,d) mkstr(mkstr(# c)  d)
    你会继续悲摧。摘抄标准的原话如下:
    Each # preprocessing token in the replacement list for a function-like macro shall be
    followed by a parameter as the next preprocessing token in the replacement list.
    大意是说,上述替换会发生在其作为参数的预定宏处理之前进行。简答的说,
    mkstr(mkstr(# c)  d) 先发生了
    # mkstr(# c) d
    这个动作。此时mkstr并不会作为一个宏再次使用,而对应# c本身也是先发生的。由此出现
    mkstr(mkstr(# init_flag)  =%d)
    -->
    # mkstr("init_flag") = %d
    mkstr("init_flag") = %d会当作一个整体字符串处理。则有
    -->
    "mkstr("init_flag") = %d“
    的情况。
    记得# 是先展开,再作为参数给予另一个宏进行展开。因此,对于#的嵌套。你想多层"",则需要
    #define mkstr(a) # a
    #define mkstr2(a) mkstr(# a)
    #define mkstr3(a) mkstr2(# a)
    这和
    #define mkstr3(a) mkstr(# mkstr(a))
    是不一样的效果。
    但同时你要注意,此时生成的并不是""init_flag"",如果你使用mkstr3(init_flag)。你可以测试一下。
    现在我们考虑一个问题,上面使用了%d。但是我们可能还有字符串的变量,因此我们可以将代码修改如下
#define __VARLOGd(exp) do {printf(__UNION_EXP2STR(exp  , = %d),exp);printf("\n");}while (0)
#define __VARLOGs(exp) do {printf(__UNION_EXP2STR(exp  , = %s),exp);printf("\n");}while (0)
    那么对应测试代码替换如下:
int init_all(void){
    char test[] = "abc";
    if (init_flag == 1) {return init_flag;}
    init_flag = 1;
    init_flag = init_flag & init_status();
    init_flag = init_flag & init_model();
    init_flag = init_flag & init_control();
    init_flag = init_flag & init_view();
    init_flag = init_flag & init_MVbuf();
    
    __VARLOGs(test);
    __VARLOGd(init_flag);
    
    return init_flag;
}
    尝试编译,链接,运行,看看效果。
    不过上面的两个宏很相似,我们是否可以再抽象一点?完全可以。如下
#define __VARLOG(exp,mode) do {printf(__UNION_EXP2STR(exp  , = mode),exp);printf("\n");}while (0)
#define __VARLOGd(exp) __VARLOG(exp,%d)
#define __VARLOGs(exp) __VARLOG(exp,%s)
鬼话:宏的妙用需要你有高度的抽象感,抽象感的锻炼,反复精简宏,是个不错的方法。
    但是这里 %d ,%s还是有问题。一个简单的道理,我们对指针怎么处理?指针在32位系统,和64位系统下,有不同的表现。%d通常是针对
    32位,int型。而ld针对long int ,lld 针对 long long int,而每个系统对于特别是后者,有不同的解释。那么我们需要如下设计
#if sizeof(int) == 32
typedef int32 int
#elif sizeof(long int) == 32
typedef int32 long int
#endif
    
#define __VARLOG(exp,mode,type) do {printf(__UNION_EXP2STR(exp  , = mode),(type)exp);printf("\n");}while (0)
#define __VARLOGi32(exp) __VARLOG(exp,%d,int32)
#define __VARLOGi64(exp) __VARLOG(exp,%ld,int64)
#define __VARLOGs(exp) __VARLOG(exp,%s,char*)

    现在的代码描述并没有什么问题。但确有不好的地方,我们分别讨论一下。
    = mode mode是什么, = mode是什么,描述的不清晰,其实 =只是用于输出,而mode是用于格式,为了凸显他们的差异性,我们可以尝试如下描述
    (exp , = _FMT)
    而另一方面,其实type 在__VARLOG中有一个完整的含义描述,就是对exp尽心类型转换。而(type)exp表示,存在类型转换的存储空间(变量)。因此可以如下写法。
#define __TYPE_C(type,var) (type)(var)
#define __TYPE_I32(var) __TYPE_C(i32,var)
#define __VARLOG(exp,_FMT,var) do {printf(__UNION_EXP2STR(exp, = _FMT),var);printf("\n");} while (0)
#define __VARLOGi32(exp) __VARLOG(exp,%d,__TYPE_I32(exp))    
    由此,整体define_attr.h的清单如下:
#ifndef _DEFINE_ATTR_H_
#define _DEFINE_ATTR_H_
#include <stdio.h>
typedef int i32;
typedef long int i64;

#define __PRINT_POS() do {fprintf(stderr,"%s(%d){%s}\n",__FILE__,__LINE__,__func__);}while(0)    
#define __PRINT_FUNC()  __PRINT_POS()  //do {printf("%s func!\n",__func__);}while(0)

#define mkstr(a) # a

#define __UNION_EXP2STR(c,d) mkstr(c  d)

#define __TYPE_V(type,var) (type)(var)
#define __TYPE_I32(var) __TYPE_V(i32,var)
#define __TYPE_S(var) __TYPE_V(char *,var)
#define __VARLOG(exp,_FMT,var) do {printf(__UNION_EXP2STR(exp, = _FMT),var);printf("\n");} while (0)
#define __VARLOGi32(exp) __VARLOG(exp,%d,__TYPE_I32(exp))    
#define __VARLOGs(exp) __VARLOG(exp,%s,__TYPE_S(exp))
#endif    
    这样写,替换来替换去,究竟有什么好处?本质上,最终在代码展开后,都一样。但对于代码阅读和代码再利用,则不一样。
    假设你如下写
#define __VARLOGi32(exp) __VARLOG(exp,%d,__TYPE_V(i32,exp))    
#define __VARLOGs(exp) __VARLOG(exp,%s,__TYPE_V(char *,exp))    
    确实没问题,但是在代码的其他位置,    可能你仍然有类型转换的要求,诸如(假设以下代码, pdst ,psrc都是32位对齐:
#define __COPY32(pdst,psrc,n) do{\
    i32 *pd = (i32 *)pdst;\
    i32 *ps = (i32 *)psrc;\
    char *pdc ,*psc;\
    while (n >= 4){\
        *pd++ = *ps++;\
        n-=4;\
    }    \
    pdc = (char *)pd;    psc = (char *)ps;\
    while (n > 0){\
        *pdc++ = *psc++; n--;\
    }\
while (0)
    这样写有点累。因为你为了使用不同位宽的指针,由此导致存在不同的类型转换,甚至你需要书写一堆临时存储空间,虽然这些空间在优化
    后基本没有实际的存储空间,无非影响到按什么方式读取数据的代码的修正,例如是读取一个16位的数据,还是读取一个32位的数据,甚至
    为了读取一个8位的数据,在读取完32位后,还要截取。那么我们其实可以如下设计代码。
    while (n >= 4){
        *(i32 *)pdst = *(i32 *)psrc;
        pdst = (i32 *)pdst + 1; psrc = (i32*) psrc + 1;
        n -= 4 ;
    }
    while (n>0){
        *(char *)pdst = *(char *)psrc;
        pdst = (char *)pdst + 1; psrc = (char*) psrc + 1;
        n -- ;
    }
    这样书写,比较清晰,但容易出错,效率上,在优化后,和上面的方式并没有差异。但这样写极易容易隐藏错误。隐藏书写设计的错误。
鬼话:重复一句,最好,最高层次的检测错误的方法,就是在设计阶段,约束,回避错误的发生,而不是事后补漏。事后即便找到错,但也可能因为局部的观测,而忽略的整体的设计特性,导致错是改了,系统散了。
    可能会有这样的错误。
    while (n>0){
        *(char *)pdst = *(char *)psrc;
        pdst = (char *)pdst + 1; psrc = (int*) psrc + 1;
        n -- ;
    }    
    别问我这么简单的错误,怎么可能发生。至少的经验,无论我本人,还是同事,朋友,很多长时间debug出来的错误都是低级错误。那么一种规避的方式可以如下设计。
#define NEXT_T(_T,d) do { pd = __TYPE_V(void *,__TYPE_V(_T,pd) + 1);}while (0)
#define COPY_T(_T,pd,ps) do{*__TYPE_V(_T,pd) = *__TYPE_V(_T,ps); } while (0)
#define COPY_T_NEXT(_T,pd,ps) do {COPY_T(_T,pd,ps); NEXT_T(_T,pd); NEXT_T(_T,ps);}while (0)
    while (n >= 4){
        COPY_T_NEXT(i32*,pdst,psrc);
        n-=4;
    }
    while (n > 0){
        COPY_T_NEXT(char *,pdst,psrc);
        n--;
    }

    上述这样做的好处是,当第一个测试通过,第二个,测试也没有问题,即便i32和char不同。

未完,待续。。。。

   
加载中
0
solu
solu
更新好快呀,才开始看第六篇的路过!
0
疯人院主任
疯人院主任
我始终迟迟在第一篇
0
景德真人
景德真人
一时找不到
0
justjavac
justjavac
不好消化,得慢慢看。
0
eatapple
eatapple
求个顺序啊,我都找不到按顺序排列的。想从第一篇开始看。
0
每天打起精神即可
每天打起精神即可

引用来自“eatapple”的答案

求个顺序啊,我都找不到按顺序排列的。想从第一篇开始看。
eatapple
eatapple
谢谢
0
defu
defu
野鬼大哥, #define __PRINT_FUNC() do {printf("%s func!\n",__func__);}while(0)为什么要加一个do..while(0)呢,看过不少代码,都有这个习惯,是为了提高效率吗?
coolge
coolge
这个好像是为了保证执行一次代码
0
v
viking
有的人写if不加{},如果宏里有不止一句就悲剧了。
0
中山野鬼
中山野鬼

引用来自“defu”的答案

野鬼大哥, #define __PRINT_FUNC() do {printf("%s func!\n",__func__);}while(0)为什么要加一个do..while(0)呢,看过不少代码,都有这个习惯,是为了提高效率吗?
这样可以保障宏在调用时,和其他语法的书写一致性,以及规避写错误。
返回顶部
顶部