利用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语言的三点语言特性:
- C语言里面,任何一个语句(statement)都是有值的;
- C语言可以自动将数值类型隐式转换成逻辑值:真(true)或假(false);
- 在逻辑运算中,之前的运算已经可以断定逻辑结果,后边的运算将不再继续进。
"任何一个语句都是有值"
举例来说:
赋值语句(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的函数了。