打印

C语法

[复制链接]
楼主: wenfengcom
手机看帖
扫描二维码
随时随地手机跟帖
41
leizi2008| | 2009-9-23 15:54 | 只看该作者 回帖奖励 |倒序浏览
先判断

使用特权

评论回复
42
leizi2008| | 2009-9-23 16:04 | 只看该作者
:lol存在必有其合理性,如果用的方便就用

使用特权

评论回复
43
codycody23| | 2009-9-23 18:06 | 只看该作者
k&r 里就这么写。。。。

一个不错的总结。
-----------------------------------------------------------------------------------------
3. Side Effect与Sequence Point 请点评
如果你只想规规矩矩地写代码,那么基本用不着看这一节。本节的内容基本上是钻牛角尖儿的,除了Short-circuit比较实用,其它写法都应该避免使用。但没办法,有时候不是你想钻牛角尖儿,而是有人逼你去钻牛角尖儿。这是我们的学员在找工作笔试时碰到的问题:

int a=0;
a = (++a)+(++a)+(++a)+(++a);
据我了解,似乎很多公司都有出这种笔试题的恶趣味。答案应该是Undefined,我甚至有些怀疑出题人是否真的知道答案。下面我来解释为什么是Undefined。

我们知道,调用一个函数可能产生Side Effect,使用某些运算符(++ -- = 复合赋值)也会产生Side Effect,如果一个表达式中隐含着多个Side Effect,究竟哪个先发生哪个后发生呢?C标准规定代码中的某些点是Sequence Point,当执行到一个Sequence Point时,在此之前的Side Effect必须全部作用完毕,在此之后的Side Effect必须一个都没发生。至于两个Sequence Point之间的多个Side Effect哪个先发生哪个后发生则没有规定,编译器可以任意选择各Side Effect的作用顺序。下面详细解释各种Sequence Point。

1、调用一个函数时,在所有准备工作做完之后、函数调用开始之前是Sequence Point。比如调用foo(f(), g())时,foo、f()、g()这三个表达式哪个先求值哪个后求值是Unspecified,但是必须都求值完了才能做最后的函数调用,所以f()和g()的Side Effect按什么顺序发生不一定,但必定在这些Side Effect全部作用完之后才开始调用foo函数。

2、条件运算符?:、逗号运算符、逻辑与&&、逻辑或||的第一个操作数求值之后是Sequence Point。我们刚讲过条件运算符和逗号运算符,条件运算符要根据表达式1的值是否为真决定下一步求表达式2还是表达式3的值,如果决定求表达式2的值,表达式3就不会被求值了,反之也一样,逗号运算符也是这样,表达式1求值结束才继续求表达式2的值。

逻辑与和逻辑或早在第 3 节 “布尔代数”就讲了,但在初学阶段我一直回避它们的操作数求值顺序问题。这两个运算符和条件运算符类似,先求左操作数的值,然后根据这个值是否为真,右操作数可能被求值,也可能不被求值。比如例 8.5 “剪刀石头布”这个程序中的这几句:

ret = scanf("%d", &man);
if (ret != 1 || man < 0 || man > 2) {
        printf("Invalid input!\n");
        return 1;
}
其实可以写得更简单(类似于[K&R]的简洁风格):

if (scanf("%d", &man) != 1 || man < 0 || man > 2) {
        printf("Invalid input!\n");
        return 1;
}
这个控制表达式的求值顺序是:先求scanf("%d", &man) != 1的值,如果scanf调用失败,则返回值不等于1成立,||运算有一个操作数为真则整个表达式为真,这时直接执行下一句printf,根本不会再去求man < 0或man > 2的值;如果scanf调用成功,则读入的数保存在变量man中,并且返回值等于1,那么说它不等于1就不成立了,第一个||运算的左操作数为假,就会去求右操作数man < 0的值作为整个表达式的值,这时变量man的值正是scanf读上来的值,我们判断它是否在[0, 2]之间,如果man < 0不成立,则整个表达式scanf("%d", &man) != 1 || man < 0 的值为假,也就是第二个||运算的左操作数为假,所以最后求右操作数man > 2的值作为整个表达式的值。

&&运算与此类似,a && b的计算过程是:首先求表达式a的值,如果a的值是假则整个表达式的值是假,不会再去求b的值;如果a的值是真,则下一步求b的值作为整个表达式的值。所以,a && b相当于“if a then b”,而a || b相当于“if not a then b”。这种特性称为Short-circuit,很多人喜欢利用Short-circuit特性简化代码。

3、在一个完整的声明末尾是Sequence Point,所谓完整的声明是指这个声明不是另外一个声明的一部分。比如声明int a[10], b[20];,在a[10]末尾是Sequence Point,在b[20]末尾也是。

4、在一个完整的表达式末尾是Sequence Point,所谓完整的表达式是指这个表达式不是另外一个表达式的一部分。所以如果有f(); g();这样两条语句,f()和g()是两个完整的表达式,f()的Side Effect必定在g()之前发生。

5、在库函数即将返回时是Sequence Point。这条规则似乎可以包含在上一条规则里面,因为函数返回时必然会结束掉一个完整的表达式。而事实上很多库函数是以宏定义的形式实现的(第 2.1 节 “函数式宏定义”),并不是真正的函数,所以才需要有这条规则。

还有两种Sequence Point和某些C标准库函数的执行过程相关,此处从略,有兴趣的读者可参考[C99]的Annex C。

现在可以分析一下本节开头的例子了。a = (++a)+(++a)+(++a)+(++a);的结果之所以是Undefined,因为在这个表达式中有五个Side Effect都在改变a的值,这些Side Effect按什么顺序发生不一定,只知道在整个表达式求值结束时一定都发生了。比如现在求第二个++a的值,这时第一个、第三个、第四个++a的Side Effect发生了没有,a的值被加过几次了,这些都不确定,所以第二个++a的值也不确定。这行代码用不同平台的不同编译器来编译结果是不同的,甚至在同一平台上用同一编译器的不同版本来编译也可能不同。

写表达式应遵循的原则一:在两个Sequence Point之间,同一个变量的值只允许被改变一次。仅有这一条原则还不够,例如a[i++] = i;的变量i只改变了一次,但结果仍是Undefined,因为等号左边改i的值,等号右边读i的值,到底是先改还是先读?这个读写顺序是不确定的。但为什么i = i + 1;就没有歧义呢?虽然也是等号左边改i的值,等号右边读i的值,但你不读出i的值就没法计算i + 1,那拿什么去改i的值呢?所以这个读写顺序是确定的。写表达式应遵循的原则二:如果在两个Sequence Point之间既要读一个变量的值又要改它的值,只有在读写顺序确定的情况下才可以这么写。


-----------------------------------------------------------------------------------------
4. 运算符总结
到此为止,除了和指针相关的运算符还没讲之外,其它运算符都讲过了,是时候做一个总结了。

运算符+ - * / % > < >= <= == != & | ^ 以及各种复合赋值运算符要求两边的操作数类型一致,条件运算符?:要求后两个操作数类型一致,这些运算符在计算之前都需要做Usual Arithmetic Conversion。

下面按优先级从高到低的顺序总结一下C语言的运算符,每一条所列的各运算符具有相同的优先级,对于同一优先级的多个运算符按什么顺序计算也有说明,双目运算符就简单地用“左结合”或“右结合”来说明了。和指针有关的运算符* & ->也在这里列出来了,到第 23 章 指针再详细解释。

1、标识符、常量、字符串和用()括号套起来的表达式是组成表达式的最基本单元,在运算中做操作数,优先级最高。

2、后缀运算符,包括数组取下标[]、函数调用()、结构体取成员“.”、指向结构体的指针取成员->、后缀自增++、后缀自减--。如果一个操作数后面有多个后缀,按照离操作数从近到远的顺序(也就是从左到右)依次计算,比如a.name++,先算a.name,再++,这里的.name应该看成a的一个后缀,而不是把.看成双目运算符。

3、单目运算符,包括前缀自增++、前缀自减--、sizeof、类型转换()、取地址运算&、指针间接寻址*、正号+、负号-、按位取反~、逻辑非!。如果一个操作数前面有多个前缀,按照离操作数从近到远的顺序(也就是从右到左)依次计算,比如!~a,先算~a,再求!。

4、乘*、除/、模%运算符。这三个运算符是左结合的。

5、加+、减-运算符。左结合。

6、移位运算符<<和>>。左结合。

7、关系运算符< > <= >=。左结合。

8、相等性运算符==和!=。左结合。

9、按位与&。左结合。

10、按位异或^。左结合。

11、按位或|。左结合。

12、逻辑与&&。左结合。

13、逻辑或||。左结合。

14、条件运算符:?。在第 2 节 “if/else语句”讲过Dangling-else问题,条件运算符也有类似的问题。例如a ? b : c ? d : e是看成(a ? b : c) ? d : e还是a ? b : (c ? d : e)呢?C语言规定是后者。

15、赋值=和各种复合赋值(*= /= %= += -= <<= >>= &= ^= |=)。在双目运算符中只有赋值和复合赋值是右结合的。

16、逗号运算符。左结合。

[K&R]第2章也有这样一个列表,但是对于结合性解释得不够清楚。左结合和右结合这两个概念只对双目运算符有意义,对于前缀、后缀和三目运算符我单独做了说明。C语言表达式的详细语法规则可以参考[C99]的Annex A.2,其实语法规则并不是用优先级和结合性这两个概念来表述的,有一些细节用优先级和结合性是表达不了的,只有看C99才能了解完整的语法规则。

使用特权

评论回复
44
aceice| | 2009-9-24 12:15 | 只看该作者
刚学C语言的时候,喜欢缩写。
现在反而不喜欢缩写了。宁可多写几句,加个括号等看似麻烦的事。

使用特权

评论回复
45
SkyCode| | 2009-9-24 17:17 | 只看该作者
看不懂的写法,不是好写法。
程序匠人 发表于 2009-8-4 00:25

使用特权

评论回复
46
huangqi412| | 2009-9-24 19:09 | 只看该作者
多用括号,除非学校的考试

使用特权

评论回复
47
monitor| | 2009-9-24 21:25 | 只看该作者
很多人被教科书蒙骗了,书上只是用这个写法来说明运算符的优先级,而实际应用时要看编译器怎么做,比较烦。

使用特权

评论回复
48
monitor| | 2009-9-24 21:28 | 只看该作者
这种写法的功能是用来开阔思路,编程时不用为好

使用特权

评论回复
49
yuanwnequn| | 2009-9-24 22:08 | 只看该作者
应该是先判断啦,从右到左,不过连我都不会这样写的

使用特权

评论回复
50
sjl2006| | 2009-9-25 08:43 | 只看该作者
简单即是美~~
如果我们是做工程的,而不是做学术的,建议记住以下“三不”:
不要挑战自己的记性;
不要挑战同事的耐心;
不要挑战编译器的水平。

使用特权

评论回复
51
香水城| | 2009-9-25 18:21 | 只看该作者
一个**这个问题的小窍门:如果++或--在变量的前面,表示做递加或递减运算并回存结果,再用递加或递减结果参与其它的计算;如果++或--在变量的后面,则先使用变量的数值参与其它的计算,随后再把递加或递减结果存回这个变量。

使用++或--的好处是可以使程序简练。如果楼主的问题不这样写,而是分开写,则是这样:
if(temp==0)  {
  temp--;
  ......
}
else {
  temp--;
  ......
}

使用--,则可以这样写:
if(temp--==0)  {
  ......
}
else {
  ......
}

估计当初是为了某种优化指令而设计了这2个操作。

使用特权

评论回复
52
徐小剑| | 2009-9-25 19:06 | 只看该作者
根据c语言的用法应该就是先判断temp是否等于0再进行自减

使用特权

评论回复
53
hyg1984| | 2009-9-26 13:06 | 只看该作者
很多人的心声啊,呵呵,如果考试了,讲技巧还差不多
实际应用中,讲的是实用,通俗易懂。
不过C语言中有讲,“temp--”这一句代码中,temp的值将不改变。只有执行完这段代码
后,temp的值才减1,所以------应该是先判断=0,执行完if语句之后,temp的值才减1。
如果是想先减1再判断=0的话,可以if(--temp == 0 )
但是谁会去做不保险的事呢,所以宁可if((temp--) == 0)加多个括号,也不玩弄技巧!

使用特权

评论回复
54
冷漠| | 2009-9-26 20:44 | 只看该作者
本帖最后由 冷漠 于 2009-9-26 20:51 编辑

掌握基本概念就好理解了。以前咱也搞不清楚,还总抱怨搞不清谁的优先级高,今天终于掌握了“诀窍”。
刚学的基本概念,现学现卖:

“i ++ 表示先使用 i 的值,然后++  。”  (呵呵,什么叫“先使用 i 的值”?)
“++ i 表示先++ ,然后使用 i  的值。”   

所以,LZ的问题不用实验,肯定是16楼说的“先判断,然后减。”(先使用temp的值,后考虑++ 。)

使用特权

评论回复
55
john_lee| | 2009-9-27 02:58 | 只看该作者
我肯定会这么写if(temp-- == 0),而不会写成像51楼香版所展开的那样,想必香版也不会把它展开写吧。

if(temp-- == 0)才是正真讲效率的写法(特别在嵌入式软件里),不管是对阅读者还是对编译器来说,都是有好处的。

首先是阅读者,高手是肯定看不惯展开的写法,当他/她看到if表达式后出现了一个语句temp--,而在else后又出现了语句temp--,他/她会觉得非常不舒服:语句(表达式)有冗余,这明明可以合成一步放在if表达式中。有TX要拍砖了:他/她为什么会觉得不舒服?他/她也有可能喜欢冗余啊。我下面会解释的(自己先纳闷一下:!@#$%^,有人喜欢冗余?)。

其二,编译器,在年代久远的时候,那时候整个编译优化技术水平还相当低下,对这种展开的写法基本无法优化(表达式跨逻辑合并),编译结果就是两个temp--都编译出来了(实际的汇编指令),但使用者希望两个表达式合并,只编译出一个。这要求相当合理。C语言的创造者认为,既然编译器做不到这种优化,那么就引进一种语法,让使用者自己去合并表达式。这就是++、--的由来。这样一来,编译器也省心了,来一个表达式,编译一个表达式,来两个表达式,就编译两个表达式。

虽然当前的编译优化技术发展到了一个很高的高度,但想要完全扔掉++、--而不损失一点效率,还是不行的。

在现实中,有的人不懈地追求效率(这是一个人生态度),他们在算法、程序设计方面也是如此。一旦有一个语法(或者一个算法)可以让他们的程序提高效率,他们是无论如何也不会拒绝的,而只会努力熟悉它、掌握它,不让它产生负面影响。那么,可以想象,日积月累,形成习惯以后,如果再让他们去看那种冗余的编程写法,他们是多么的不舒服!也可以想象,这些人凭着他们精益求精的态度,必定都会有所成就。

本贴中反对使用++、--的TX,难道你们认为自己水平高过那些创造++、--和熟练使用++、--的大牛们吗?

附带描述一下if(temp--==0)最优化的编译结果:
1、减法指令:temp -= 1。说明:由于做了减法,如果之前temp的值为0,那么状态寄存器的C标志置1,否则C标志清0。(这步在大多数CPU上只需要1条指令)
2、条件分枝指令:如果C == 0,则跳转到else。(这步在大多数CPU上只需要1条指令)

TX们,看看吧,不用--,能达到这么高的效率吗?而且,编译结果是不是像你们想象的那样先比较,再做减法,然后跳转呢?

使用特权

评论回复
56
myfaith| | 2009-9-27 09:31 | 只看该作者
++ 和-- 的首先级别是最高的,比括号还高,

如果 X =5;y =(x++) *(x--);那么,结果y就等于 25

如果 X =5;y =(++x) *(x);那么,结果y就等于 30 ...
呆板书生 发表于 2009-9-23 10:07

这个例子不能说明++/--比()优先级高,仅仅说明++在前与在后的区别而已.++/--与()优先级同等

使用特权

评论回复
57
hhhhhhgggg| | 2009-12-15 15:15 | 只看该作者
支持51L

使用特权

评论回复
58
yeshaozhu| | 2009-12-15 21:48 | 只看该作者
强烈建议不要这么写!
我很赞同有几楼说的:这种东西只是在考试的时候会用,只有教科书上喜欢这样写(可能是说明优先级的必要吧)。
事实上这样的写法不但没有技巧,反而很容易弄错,也不便于阅读。其实吧,我现在觉得对于程序来说,结构清晰最重要了,代码的繁简关键要看你的思路和算法

使用特权

评论回复
59
zzz172228435| | 2009-12-15 21:57 | 只看该作者
考试过客解答
++i,--i(在使用i之前,先使i得值加(减)1)
i++,i--(在使用i之后,使i得值加(减)1)
所以LZ所问为先比较后自减1

使用特权

评论回复
60
xlsbz| | 2009-12-15 22:02 | 只看该作者
哦 是这样的  我还没有用过--  我只用过++:P

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则