C语言运算符的优先级
一、运算符的优先级表
C 语言的符号众多,由这些符号又组合成了各种各样的运算符。既然是运算符就一定有其特定的优先级,下表就是C 语言运算符的优先级表:上表不容易记住。其实也用不着死记,用得多,看得多自然就记得了。也有人说不用记这些东西,只要记住乘除法的优先级比加减法高就行了,别的地方一律加上括号。这在你自己写代码的时候,确实可以,但如果是你去阅读和理解别人的代码呢?别人不一定都加上括号了吧?所以,记住这个表,我个人认为还是很有必要的。
二、一些容易出错的优先级问题
上表中,优先级同为1 的几种运算符如果同时出现,那怎么确定表达式的优先级呢?这是很多初学者迷糊的地方。下表就整理了这些容易出错的情况:这些容易出错的情况,希望读者好好在编译器上调试调试,这样印象会深一些。一定要多调试,光靠看代码,水平是很难提上来的。调试代码才是最长水平的。
2/(-2)的值是多少?
除法运算在小学就掌握了的,这里还要讨论什么呢?别急,先计算下面这个例子:2/(-2)的值为多少?2%(-2)的值呢?如果与你想象的结果不一致,不要惊讶。我们先看看下面这些规则:
假定我们让a 除以b,商为q,余数为r:
q = a/b;
r = a%b;
这里不妨先假定b 大于0。
我们希望a、b、q、r 之间维持什么样的关系呢?
1,最重要的一点,我们希望q*b + r == a,因为这是定义余数的关系。
2,如果我们改变a 的正负号,我们希望q 的符号也随之改变,但q 的绝对值不会变。
3,当b>0 时,我们希望保证r>=0 且r<b。
这三条性质是我们认为整数除法和余数操作所应该具备的。但是,很不幸,它们不可能同时成立。
先考虑一个简单的例子:3/2,商为1,余数也为1。此时,第一条性质得到了满足。好,把例子稍微改写一下:(-3)/2 的值应该是多少呢?如果要满足第二条性质,答案应该是-1。但是,如果是这样,余数就必定是-1,这样第三条性质就无法满足了。如果我们首先满足第三条性质,即余数是1,这种情况下根据第一条性质,商应该为-2,那么第二条性质又无法满足了。
上面的矛盾似乎无法解决。因此,C 语言或者其他语言在实现整数除法截断运算时,必须放弃上述三条性质中的至少一条。大多数编程语言选择了放弃第三条,而改为要求余数与被除数的正负号相同。这样性质1 和性质2 就可以得到满足。大多数C 语言编译器也都是如此。
但是,C 语言的定义只保证了性质1,以及当a>=0 且b>0 时,保证|r|<|b|以及r>=0。后面部分的保证与性质2 或性质3 比较起来,限制性要弱得多。通过上面的解释,你是否能准确算出2/(-2)和2%(-2)的值呢?
C语言++、--操作符
这绝对是一对让人头疼的兄弟。先来点简单的:int i = 3;
(++i)+(++i)+(++i);
表达式的值为多少?15 吗?16 吗?18 吗?其实对于这种情况,C语言标准并没有作出规定。有点编译器计算出来为18,因为i 经过3 次自加后变为6,然后3 个6 相加得18;而有的编译器计算出来为16(比如Visual C++6.0),先计算前两个i 的和,这时候i 自加两次,2 个i 的和为10,然后再加上第三次自加的i 得16。其实这些没有必要辩论,用到哪个编译器写句代码测试就行了。但不会计算出15 的结果来的。
++、--作为前缀,我们知道是先自加或自减,然后再做别的运算;但是作为后缀时,到底什么时候自加、自减?这是很多初学者迷糊的地方。假设i=0,看例子:
A)
j =(i++,i++,i++);
B)
for(i=0;i<10;i++)
{
//code
}
C)
k = (i++)+ (i++)+ (i++);
你可以试着计算他们的结果。
A) 例子为逗号表达式,i 在遇到每个逗号后,认为本计算单位已经结束,i 这时候自加。关于逗号表达式与“++”或“--”的连用,还有一个比较好的例子:
int x;
int i = 3;
x = (++i, i++, i+10);
问x 的值为多少?i 的值为多少?
按照上面的讲解,可以很清楚的知道,逗号表达式中,i 在遇到每个逗号后,认为本计算单位已经结束,i 这时候自加。所以,本例子计算完后,i的值为5,x的值为15。
B) 例子i 与10 进行比较之后,认为本计算单位已经结束,i 这时候自加。
C) 例子i 遇到分号才认为本计算单位已经结束,i 这时候自加。
也就是说后缀运算是在本计算单位计算结束之后再自加或自减。C 语言里的计算单位大体分为以上3 类。
留一个问题:
for(i=0,printf(“First=%d”,i);
i<10,printf(“Second=%d”,i);
i++,printf(“Third=%d”,i))
{
printf(“Fourth=%d”,i);
}
打印出什么结果?
一、++i+++i+++i
上面的例子很简单,那我们把括号去掉看看:int i = 3;
++i+++i+++i;
天啦!这到底是什么东西?好,我们先看看这个:a+++b 和下面哪个表达式想当:
A)
a++ +b;
B)a+ ++b;
二、贪心法
C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。 这个处理的策略被称为“贪心法”。需要注意到是,除了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行符等)。比如:==是单个符号,而= =是两个等号。按照这个规则可能很轻松的判断a+++b 表达式与a++ +b 一致。那++i+++i+++i;会被解析成什么样子呢?希望读者好好研究研究。另外还可以考虑一下这个表达式的意思:a+++++b;
C语言花括号{}
花括号每个人都见过,很简单吧。但曾经有一个学生问过我如下问题:char a[10] = {“abcde”};
他不理解为什么这个表达式正确。我让他继续改一下这个例子:
char a[10] { = “abcde”};
问他这样行不行。那读者以为呢?为什么?
花括号的作用是什么呢?我们平时写函数,if、while、for、switch 语句等都用到了它,但有时又省略掉了它。简单来说花括号的作用就是打包。你想想以前用花括号是不是为了把一些语句或代码打个包包起来,使之形成一个整体,并与外界绝缘。这样理解的话,上面的问题就不是问题了。
C语言位运算符
C 语言中位运算包括下面几种:- & 按位与
- | 按位或
- ^ 按位异或
- ~ 取反
- << 左移
- >> 右移
a ^= b; b ^= a;a ^= b;但并不推荐这么做,因为这样的代码读起来很费劲。
一、左移和右移
- 左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数,高位丢弃,低位补0。
- 右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位,“>>”右边的数指定移动的位数。但注意:对于有符号数,在右移时,符号位将随同移动。当为正数时, 最高位补0;而为负数时,符号位为1,最高位是补0 或是补1 取决于编译系统的规定。Turbo C 和很多系统规定为补1。
二、0x01<<2+3 的值为多少?
再看看下面的例子:0x01<<2+3;
结果为7 吗?测试一下。结果为32?别惊讶,32 才是正确答案。因为“+”号的优先级比移位运算符的优先级高(关于运算符的优先级,我并不想在这里做过多的讨论,你几乎可以在任何一本C 语言书上找到)。好,在32 位系统下,再把这个例子改写一下:
0x01<<2+30;或0x01<<2-3;
这样行吗?不行。一个整型数长度为32 位,左移32 位发生了什么事情?溢出!左移-1位呢?反过来移?所以,左移和右移的位数是有讲究的。左移和右移的位数不能大于数据的长度,不能小于0。
C语言逻辑运算符||和&
||和&&是我们经常用到的逻辑运算符,与按位运算符|和&是两码事。下一节会介绍按位运算符。虽然简单,但毕竟容易犯错。看例子:int i=0;
int j=0;
if((++i>0)||(++j>0))
{
//打印出i 和j 的值。
}
结果:i=1;j=0。
不要惊讶。逻辑运算符||两边的条件只要有一个为真,其结果就为真;只要有一个结果为假,其结果就为假。if((++i>0)||(++j>0))语句中,先计算(++i>0),发现其结果为真,后面的(++j>0)便不再计算。同样&&运算符也要注意这种情况。这是很容易出错的地方,希望读者注意。
C语言单引号、双引号
我们知道双引号引起来的都是字符串常量,单引号引起来的都是字符常量。但初学者还是容易弄错这两点。比如:‘a’和“a”完全不一样,在内存里前者占1 个byte,后者占2个byte。关于字符串常量在指针与数组那章将有更多的讨论。这两个列子还好理解,再看看这三个:
1,‘1‘,“1”。
第一个是整形常数,32 位系统下占4 个byte;
第二个是字符常量,占1 个byte;
第三个是字符串常量,占2 个byte。
三者表示的意义完全不一样,所占的内存大小也不一样,初学者往往弄错。字符在内存里是以ASCAII 码存储的,所以字符常量可以与整形常量或变量进行运算。如:‘A‘ + 1。
C语言接续符和转义符
C 语言里以反斜杠(\)表示断行。编译器会将反斜杠剔除掉,跟在反斜杠后面的字符自动接续到前一行。但是注意:反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。当然你可以测试一下加了空格之后的效果。我们看看下面的例子://这是一条合法的\
单行注释
/\
/这是一条合法的单行注释
#def\
ine MAC\
RO 这是一条合法的\
宏定义
cha\
r* s="这是一个合法的\\
n 字符串";
反斜杠除了可以被用作接续符,还能被用作转义字符的开始标识。常用的转义字符及其含义:
\n 回车换行
\t 横向跳到下一制表位置
\v 竖向跳格
\b 退格
\r 回车
\f 走纸换页
\\ 反斜扛符"\"
\' 单引号符
\a 鸣铃
\ddd 1~3 位八进制数所代表的字符
\xhh 1~2 位十六进制数所代表的字符
广义地讲,C 语言字符集中的任何一个字符均可用转义字符来表示。表中的\ddd 和\xhh正是为此而提出的。ddd 和hh 分别为八进制和十六进制的ASCII 代码。如\102 表示字母"B",\134 表示反斜线,\X0A 表示换行等。
C语言注释符号
一、几个似非而是的注释问题
C 语言的注释可以出现在C 语言代码的任何地方。这句话对不对?这是我当学生时我老师问的一个问题。我当时回答是不对。好,那我们就看看下面的例子:A)
int/*...*/i;
B)
char* s="abcdefgh //hijklmn";
C)
//Is it a \
valid comment?
D)
in/*…*/t i;
我们知道C 语言里可以有两种注释方式:/* */ 和//。那上面3 条注释对不对呢?建议你亲自在编译器中测试一下。上述前3条注释都是正确的,最后一条不正确。
A),有人认为编译器剔除掉注释后代码会被解析成inti,所以不正确。编译器的确会将注释剔除,但不是简单的剔除,而是用空格代替原来的注释。再看一个例子:
/*这是*/#/*一条*/define/*合法的*/ID/*预处理*/replacement/*指*/list/*令*/
你可以用编译器试试。
B),我们知道双引号引起来的都是字符串常量,那双斜杠也不例外。
C),这是一条合法的注释,因为\是一个接续符。关于接续符,下面还有更多讨论。
D), 前面说过注释会被空格替换,那这条注释不正确就很好理解了。
现在你可以回答前面的问题了吧?但注意:/*…*/这种形式的注释不能嵌套,如:
/*这是/*非法的*/*/
因为/*总是与离它最近的*/匹配。
二、y = x/*p
y = x/*p,这是表示x 除以p 指向的内存里的值,把结果赋值为y?我们可以在编译器上测试一下,编译器提示出错。实际上,编译器把/*当作是一段注释的开始,把/*后面的内容都当作注释内容,直到出现*/为止。这个表达式其实只是表示把x 的值赋给y,/*后面的内容都当作注释。但是,由于没有找到*/,所以提示出错。
我们可以把上面的表达式修改一下:
y = x/ *p
或者
y = x/(*p)
这样的话,表达式的意思就是x 除以p 指向的内存里的值,把结果赋值为y 了。
也就是说只要斜杠(/)和星号(*)之间没有空格,都会被当作注释的开始。这一点一定要注意。
三、怎样才能写出出色的注释
注释写得出色非常不容易,但是写得糟糕却是人人可为之。糟糕的注释只会帮倒忙。1、安息吧,路德维希.凡.贝多芬
在《Code Complete》这本书中,作者记录了这样一个故事:有位负责维护的程序员半夜被叫起来,去修复一个出了问题的程序。但是程序的原作者已经离职,没有办法联系上他。这个程序员从未接触过这个程序。在仔细检查所有的说明后,他只发现了一条注释,如下:
MOV AX 723h ;R.I.P.L.V.B.
这个维护程序员通宵研究这个程序,还是对注释百思不得其解。虽然最后他还是把程序的问题成功排除了,但这个神秘的注释让他耿耿于怀。说明一点:汇编程序的注释是以分号开头。
几个月后,这名程序员在一个会议上遇到了注释的原作者。经过请教后,才明白这条注释的意思:安息吧,路德维希.凡.贝多芬(Rest in peace, Ludwig Van Neethoven)。贝多芬于1827 年逝世,而1827 的十六进制正是723。这真是让人哭笑不得!
2、windows 大师们用注释讨论天气问题
还有个例子:前些日子windows 的源代码曾经泄漏过一部分。人们在看这部分大师的经典作品时,却发现很多与代码毫无关系的注释!有的注释在讨论天气,有的在讨论明天吃什么,还有的在骂公司和老板。这些注释虽然与代码无关,但总比上面那个让贝多芬安息的注释要强些的。至少不会让你抓狂。不过这种事情只有大师们才可以做,你可千万别用注释
讨论天气。
3、出色注释的基本要求
- 注释应当准确、易懂,防止有二义性。错误的注释不但无益反而有害。
- 边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要及时删除。
- 注释是对代码的“提示”,而不是文档。程序中的注释应当简单明了,注释太多了会让人眼花缭乱。
- 一目了然的语句不加注释。例如:i++; /* i 加1 */——多余的注释
- 对于全局数据(全局变量、常量定义等)必须要加注释。
- 注释采用英文,尽量避免在注释中使用缩写,特别是不常用缩写。因为不一定所有的编译器都能显示中文,别人打开你的代码,你的注释也许是一团乱码。还有,你的代码不一定是懂中文的人阅读。
- 注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但不可放在下方。同一结构中不同域的注释要对齐。
- 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
- 注释的缩进要与代码的缩进一致。
- 注释代码段时应注重“为何做(why)”,而不是“怎么做(how)”。说明怎么做的注释一般停留在编程语言的层次,而不是为了说明问题。尽力阐述“怎么做”的注释一般没有告诉我们操作的意图,而指明“怎么做”的注释通常是冗余的。
- 数值的单位一定要注释。注释应该说明某数值的单位到底是什么意思。比如:关于长度的必须说明单位是毫米,米,还是千米等;关于时间的必须说明单位是时,分,秒,还是毫秒等。
- 对变量的范围给出注释。
- 对一系列的数字编号给出注释,尤其在编写底层驱动程序的时候(比如管脚编号)。
- 对于函数的入口出口数据给出注释。
关于函数的注释在函数那章有更详细的讨论。
C语言符号有哪些
符号有什么好说的呢?确实,符号可说的内容要少些,但总还是有些可以唠叨地方。有一次上课,我问学生:‘/’这个符号在C 语言里都用在哪些地方?没有一个人能答完整。这说明C 语言的基础掌握不牢靠,如果真正掌握了C 语言,你就能很轻易的回答上来。这个问题就请读者试着回答一下吧。本章不会像关键字一样一个一个深入讨论,只是将容易出错的地方讨论一下。
表(2.1)标准C 语言的基本符号
你也许听说过“国际C 语言乱码大赛(IOCCC)”,能获奖的人毫无疑问是世界顶级C程序员。这是他们利用C 语言的特点极限挖掘的结果。下面这个例子就是网上广为流传的一个经典作品:
#i nclude <stdio.h>
main(t,_,a)char *a;{return!0<t?t<3?main(-79,-13,a+main(-87,1-_,
main(-86,0,a+1)+a)):1,t<_?main(t+1,_,a):3,main(-94,-27+t,a)&&t==2?_<13?main(2,_+1,"%s %d %d\n"):9:16:t<0?t<-72?main(_,t,"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l+,/n{n+,/+#n+,/#\;#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l \q#'+d'K#!/+k#;q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# \){nl]!/n{n#'; r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#n'wk nw' \
iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c \;;{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;#'rdq#w! nr'/ ') }+}{rl#'{n' ')# \}'+}##(!!/"):t<-50?_==*a?putchar(31[a]):main(-65,_,a+1):main((*a=='/')+t,_,a+1):0<t?main(2,2,"%s"):*a=='/'||main(0,main(-61,*a,"!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m.vpbks,fxntdCeghiry"),a+1);}
还没发狂?看来你抵抗力够强的。这是IOCCC 1988 年获奖作品,作者是Ian Phillipps。
毫无疑问,Ian Phillipps 是世界上最顶级的C 语言程序员之一。你可以数数这里面用了多少个符号。当然这里我并不会讨论这段代码,也并不是鼓励你也去写这样的代码(关于这段代码的分析,你可以上网查询)。恰恰相反,我要告诉你的是:大师把代码写成这样是经典,你把代码写成这样是垃圾!所以在垃圾和经典之间,你需要做一个抉择。