利用C的&&(逻辑与)和||(逻辑或)设计三元分支运算符

最近在学习Linux基础知识,从鸟哥的的Linux私房菜里学到了不少的东西。在学习Shell Script一章的时候,发现鸟哥用了这样的一个语法

test -e /dmtsai && echo "exist" || echo "Not exist"

鸟哥解释是为检测并标示/dmtsai文件夹是否存在用的。

一开始我,我想可能是&&和||在Linux的Shell Script里面有不同的含义吧,可能这样的语法相当于C语言中的条件运算符( : ? )

但是怎么想都觉得&&||在任何程序语言中都应该是逻辑运算符才是。

仔细看了后边的内容,也肯定&&和||在Shell Script里面也一样是逻辑运算符,因此再重新认真分析上边的语句之后,才发现了这个奥秘。因此我将其引入到C语言中,用&&和||来构建能够生成分支语句的宏(macro)

虽然宏在C语言中似乎都是被限制有时甚至禁止使用的,《Effective C++》一书中开篇就讲述了宏定义带来的问题。但是还是要承认宏定义技巧之高超,妙处之频出,合理利用对代码的简化还是有很多的好处的。

特别是在学习FreeBSD源码时,看到编写Unix/Linux的达人们写的用宏来做匿名结构体定义,用宏来操作指针链表的增删排序,实在是敬仰之心悠然起敬。

回到本文的主题,之所以可以利用&&和||这两个逻辑运算符来实现一个分支语句,主要是基于C语言的三点语言特性:

  1. C语言里面,任何一个语句(statement)都是有值的;
  2. C语言可以自动将数值类型隐式转换成逻辑值:真(true)或假(false);
  3. 在逻辑运算中,之前的运算已经可以断定逻辑结果,后边的运算将不再继续进。

"任何一个语句都是有值"

举例来说:

赋值语句(a=10;),它的语句值是就是赋值语句的左值,即等号左边最后得到的值;

函数调用,那么其值就是函数的返回值了;

逗号语句(a-=10,b+=1,a*=10+b;)其值就是,它的值是最后一个表达式的值,例子中最后一个表达式又是赋值语句,因此其值就是最后a的值了;

等等。

"数值类型隐式转换成逻辑值"

第二条也是比较C比较有趣的特性。如果放在判定条件(while, if)或者逻辑条件里作为参数,那么除了0,0.0,null, '\0'这些会被判定为false外,其他的无论是正数负数还是任意字符,任意其他地址值,都会被认定为true

"最短逻辑判定"

第三条主要意思是:

对于 a&&b,如果a的值是假,那么显然无论b是什么值整个结果都是假,因此b的运算将不再进行;对于a||b,如果a的值是真,b也不会进行运算了。

所以比如有

int x=10,y=8;
(++x==11)||(++y==9)

最后会发现x变成了11,而y还是8,因为++y并没有执行。

当然,如果"&&"和"||"以及"!" 同时出现在逻辑算式中,我们还要根据优先度和结合率来判定哪些语句会被忽略执行。

结合上述三点,于是就可以构建出了下边这样的宏:

#define IFF(CONDITION, STATEMENT1,STATEMENT2) \
(\
    (\
        (CONDITION)\
        &&((STATEMENT1)||1)\
    )\
    ||(STATEMENT2)\
)

其实也可以简化成如下定义:

#define IFF(CON, S1, S2) (((CON)&&((S1)||1))|| (S2))

这个宏有三个参数:CON(条件Condition),S1(语句Statement1),S2(语句Statement2)

IFF宏分析

当条件CON为真的时候,宏就需要去执行语句S1,如果S1最后的结果可以判别为真,那么整个||之前的都被认定为真,因此S2就会被忽略执行;如果S1的结果被判定为假,因为之后跟着||1,所以还是会得到真值,S2依旧会被忽略。

当条件CON为假的时候,因为是用&&连接,所以宏就会忽略S1而去执行S2,无论S2的结果被判定为真还是假,我们都不在意了,因为这个宏的目的主要是判定执行哪个语句而不是要其返回值。

IFF宏调用测试

来看一个调用的例子:

int main()
{
    int a=30, b =10;
    int max, min;
    max = min =0;
    IFF(a>b, {max = a; min = b;} , {max = b; min = a;});    
    printf("max: %d, min: %d\n",max,min);
    
    return 0;
}

最后输出

max: 30, min: 10;

从上边的程序可以看出几点:

1.语句2确实没有被执行,否则第二句的执行结果会覆盖第一句的结果;

2.可以用花括号将一段程序传给该宏来执行,而不只是简单的普通参数。

再看一个嵌套调用的例子:

int main()
{
    int a=10, b=30, c=20;
    int max;
    
        IFF(a>b
        , IFF(a>c, printf("max:%d\n", a),printf("max:%d\n",c))
        , IFF(b>c, printf("max:%d\n", b),printf("max:%d\n",c))
        );
    printf("max:%d\n",max);
        
    return 0;
}

最后的输出结果是

max:30

说明程序正确执行,也说明我们还可以直接将函数的调用传给宏。

存在的问题

C语言中,除了基本的数据类型之外,还有两种特殊的类型:自定义类型(struct)和空类型(void,但不是空指针void *).

对于void类型,一般不会用在定义数据上,多用来指示函数的返回值,用以表示该函数不需要返回值,只是执行一段代码过程。对于void类型,C就无法将其转换成逻辑真或假,因此对于返回void的函数就无法用来作为 IFF宏的实参了。

因为前面嵌套调用的例子中,printf()函数的返回值是整型,返回输出的字符数量,所以可以将该函数作为IFF宏的参数

但假如我们有如下代码

void Voidfunc()
{
    printf("I am the void function\n");    
    return;
}

int main()
{
    IFF(21, Voidfunc(), 7);
    return 0;
}

我们就会得到:

error: void value not ignored as it ought to be

因为C无法自动转换void的类型成为逻辑类型。

同样的对于结构体(struct)类型,C也无法自动将其转换成逻辑类型进行逻辑运算,因此也就无法作为IFF宏的参数了。

当然,因为IFF宏接受代码段,所以我们可以用花括号来包裹这个函数,并放上一个辅助语句来避开这个问题,即将上述调用改成

IFF(2, {Voidfunc(); 1;}, 7);

于是,我们就可以成功在IFF宏里调用返回值为void的函数了。

posted @ 2011-09-28 13:07  莫回头  阅读(214)  评论(0编辑  收藏  举报