C语言运算符的结合性详细分析.docx
《C语言运算符的结合性详细分析.docx》由会员分享,可在线阅读,更多相关《C语言运算符的结合性详细分析.docx(10页珍藏版)》请在冰豆网上搜索。
C语言运算符的结合性详细分析
C语言运算符的结合性分析
吴琼(鄂州大学计算机系,湖北鄂州)
C语言与其他高级语言相比,一个显著的特点就是其运算符特别丰富,共有34种运算符。
C语言将这34种运算符规定了不同的优先级别和结合性。
优先级是用来标识运算符在表达式中的运算顺序的,在求解表达式的值的时候,总是先按运算符的优先次序由高到低进行操作,可是,当一个运算对象两侧的运算符优先级别相同时,则按运算符的结合性来确定表达式的运算顺序。
运算符的结合性指同一优先级的运算符在表达式中操作的组织方向,即:
当一个运算对象两侧运算符的优先级别相同时,运算对象与运算符的结合顺序,C语言规定了各种运算符的结合方向(结合性)。
大多数运算符结合方向是“自左至右”,即:
先左后右,例如a-b+c,b两侧有-和+两种运算符的优先级相同,按先左后右结合方向,b先与减号结合,执行a-b的运算,再执行加c的运算。
除了自左至右的结合性外,C语言有三类运算符参与运算的结合方向是从右至左。
即:
单目运算符,条件运算符,以及赋值运算符。
关于结合性的概念在其他高级语言中是没有的,这是C语言的特点之一,特别是从右至左结合性容易出错,下面通过几个具体的运算符来剖析C语言运算符的结合性。
若a是一个变量,则++a或a++和--a或a--分别称为前置加或后置加运算和前置减或后置减运算,且++a或a++等价于a=a+1,--a或a--等价于a=a-1,即都是使该变量的值增加1或减少1。
由此可知,对一个变量实行前置或后置运算,其运算结构是相同的,但当它们与其他运算结合在一个表达式中时,其运算值就不同了。
前置运算是变量的值先加1或减1,然后将改变后的变量值参与其他运算,如x=5;y=8;c=++x*y;运算后,c的值是48,x的值是6,y的值是8。
而后置运算是变量的值先参与有关运算,然后将变量本身的值加1减1,即参加运算的是该变量变化前的值。
如x=5;y=8;c=x++*y;运算后,c的值是40,x的值是6,y的值是8。
值得注意的是,前置、后置运算只能用于变量,不能用于常量和表达式,且结合方向是从右至左。
如当i=6时,求-i++的值和i的值。
由于“-”(负号)“++”为同一个优先级,故应理解为-(i++),又因是后置加,所以先有-i++的值为-6,然后i增值1为7,即i=7。
例1main()
{inta=3,b=5,c;
c=a*b+++b;
printf(“c=%d”,c);}
要得出c的值,首先要搞清+++的含义。
++运算符的结合方向是自右向左的,如果将表达式理解为:
c=a*b+(++b);实际上C编译器将表达式处理为:
c=(a*b++)+b,因为C编译器总是从左至右尽可能多地将若干个字符组成一个运算符,如i+++j等价于(i++)+j。
接下来是解决a*b++的问题,因为++运算符的运算对象只能是整型变量而不能是表达式或常数,所以a*b++显然是a*(b++)而非(a*b)++,因此整个表达式就是c=(a*(b++))+b。
例2main()
{inti=1,j;
j=i+++i+++i++;
printf(“i=%d,j=%d\n”,i,j);}
例3main()
{inti=1,m;
m=++i+++i+++i;
printf(“i=%d,m=%d\n”,i,m);}
j和m的值均由表达式求得,并且这两个表达式均由自增运算符、加法运算符和赋值运算符组成。
那么,它们的值到底为多少呢?
j=1+1+1=3还是j=1+2+3=6?
m=2+3+4=9还是m=4+4+4=12?
上机运行结果为:
i=4,j=3,m=12。
分析:
运算符“++”,“+”和“=”的优先级是递减的,在计算时,先进行自增运算,再进行加法运算,最后是赋值运算。
而自增运算又根据“i++”和“++i”的不同定义得到不同的值。
i+++i+++i++先将i原值
(1)取出,作为表达式中i的值进行加法运算得到3,然后再实现三次自加;++i+++i+++i自加是在整个表达式求解之前开始,也即先对表达式扫描,对i进行三次自加得到4,再进行加法运算得到12。
例4
main()
{inti=1;
printf(“i=%d,j=%d\n”,i,i+++i+++i++);}
例5
main()
{inti=1
printf(“i=%d,m=%d\n”,i,++i+++i+++i);}
与前两个程序相似,只不过将表达式移到了函数中作为实参实现调用,上机运行得:
i=4,j=6,m=9。
j,m的值发生了变化。
这是因为在实现函数调用时,TurboC系统规定,如果实参中存在表达式,则按右结合性来计算实参表达式。
即运算对象先与右边的运算符结合。
i+++i+++i++即为1+2+3=6;++i+++i+++i,即为2+3+4=9。
所以当含“++”运算符的表达式作为实参实现调用时,遵守右结合性原则。
例6
设a=6,求赋值表达式a+=a-=a-a*a的值。
由于“*”(乘号)、“-”(减号)优先级高于“+=”、“-=”,且“*”优先级高于“-”,故先求a-a*a,即6-6*6=-30,由“+=”,“-=”为同一优先级,且是从右至左的结合方向,再求a-=-30,即a=a-(-30)=6+30=36,最后求a+=36,即a=a+36=36+36=72,所以赋值表达式的值为a=72。
例7
设m=1,n=2,b=3,求赋值表达式m+=n-=---b的值。
这里共有四个运算符“+=”、“-=”、“-”(负号)、“--”,由运算符优先级,应先计算---b,但“--”与“-”(负号)优先级相同,如按从右到左的结合方向,它可能是-(--b),也可能是--(-b),究竟是哪一个呢?
前面已讲过,前置运算只能用于变量,不能用于表达式,而(-b)不是一个变量,而是表达式,故只能是-(--b),即为-(3-1)=-2;然后计算n-=-2,即n=n-(-2)=2-(-2)=4;最后计算m+=4,即m=m+4=1+4=5,所以赋值表达式的值m=5。
C语言中运算符的两种不同的结合方向是其特有的,理解结合方向有助于理解表达式的运算顺序。
当看到一个复杂的C语言表达式时,首先应按优先级进行运算,然后在同一优先级中按结合方向进行运算。
而后者是初学者最易出错的,本文通过若干典型实例分析,希望对读者理解C语言运算符结合性有所帮助。
参考文献:
[1]谭浩强.C程序设计(第二版)[M].北京:
清华大学出版社,
1999.
[2]刘祎玮,汪晓平.C语言高级实例解析[M].北京:
清华大学出
版社,2004.
[3]HerbertSchildt.著,戴健鹏.译.C语言大全(第二版)[M].北京:
电子工业出版社,1994.
C语言中怎样理解三目运算符(条件运算符)的右结合性?
对于表达式--a==b++?
a++:
b++
当然是先判断--a是否等于b++,然后决定执行后面的哪个表达式。
问题:
但是它的又结合性体系那在哪里呢?
为什么不把这个表达式中所有的自加自减运算先执行完,然后再执行条件判断呢?
解析:
这里涉及了C中的优先级、结合性、求值顺序。
结合性只有在相同优先级的运算符间才起作用,比如a+b*c,+与*优先级不同,这里根本不用去管结合性。
而a+b+c则需要进行结合性考虑了,如果+为左结合性,那么应该理解为:
(a+b)+c,如果+为右结合性,那么应该理解为a+(b+c),当然了,我们已经知道+为左结合性了。
对于?
:
,在C中与它优先级相同的只有它自己,因此只有连续的?
:
才会体现出它的右结合性,即a?
b:
c?
d:
e中,根据右结合性可知应理解为a?
b:
(c?
d:
e)。
显然,上述问题中是体现不出这种结合性来的。
再者,还有一个求值顺序的问题,a?
b:
c中,C语言规定先对a求值,非零则对b求值并作为该表达式的值,为零则对c求值并作为表达式的值,并且b和c中有且仅有一个会被求值。
对于题目中的就应该是:
先进行(--a==b++)的求值,根据是否为零,会对(a++)或(b++)进行求值。
说白了,也就是只有两种可能性:
可能性1:
先算(--a==b++),再算(a++);可能性2:
先算(--a==b++),再算(b++)。
至于(--a==b++)中到底是先算--a还是b++则仅从C语言这个角度是无法判别的,这是个实现问题,如果要想写出健壮的可移植的代码就应该避免这种表述.总结一下,就是先看优先级,次看结合性,有的求值有序,有的则无序。
实际上,一条语句中出现多次同一个变量的自增或自减是不合规范的,因为这种写法出现的结果是不确定的,根据编译器而定。
a++表示语句执行后a=a+1,到底有多后?
有2个a++怎么办?
这都是编译器内部机制的问题。
真正好的程序员是避免这种情况的。
C语言运算符优先级普遍存在的一个深层次误区
国内许多C语言教科书,在介绍C语言运算符时,都把所谓的“单目运算符”归纳为优先级相同结合性从右向左的运算符。
例如号称是“我国广大初学者学习C语言程序设计的主流用书”的《C程序设计》(谭浩强著,清华大学出版社,2010年6月(第四版))的378页附录D运算符和结合性中就是这样:
殊不知,这种归纳是完全错误的。
而且恰恰由于《C程序设计》是所谓的“主流用书”,其错误带来的影响也是广泛普遍的和灾难性的。
(google或XX一下“所有的单目运算符具有相同的优先级”,你就会知道我是不是在夸大其词危言耸听)。
为了揭示“所有的单目运算符具有相同的优先级”的错误,下面首先按照这种错误的说法进行一个实验。
我们都知道,对于
inti;
来说,&i是求得一个指向i的指针(注意这里的“&”是一个“单目运算符”),&i的数据类型显然是“int*”。
如果对“int*”类型的表达式“&i”做“(int*)”类型转换运算(可能显得有点无聊)(int*)&i得到显然还是“&i”——值和类型都没有任何改变。
按照“所有的单目运算符具有相同的优先级”这个错误的说法,由于“&”和“(int*)”的结合性从右向左
(int*)&i
这个表达式没有任何毛病,也不需要通过加“()”来明确运算对象。
现在,再对
(int*)&i
这个表达式做sizeof运算,由于sizeof也和(int*)同级(注意这是错误的),结合性从右向左,所以可以直接把sizeof写在(int*)&i的左面,即
sizeof(int*)&i
显然,这个表达式的运算结果和sizeof(int*)应该一模一样,因为(int*)&i的数据类型是(int*)。
然而,如果你在机器上跑一下下面的代码的话
#include
#include
intmain(void)
{
inti ;
printf("%u",sizeof(int*) );
printf("%u",sizeof(int*)&i);
system("PAUSE");
}
这句话的究竟有什么错误呢?
错误就在于,“(类型)”这种运算符的优先级其实低于sizeof和一元&运算符。
由于类型转换运算符的优先级低于sizeof,所以
sizeof(int*)&i
不可能表示sizeof((int*)&i)这样的含义,因为C语言的优先级和结合性规定只容许运算符作用于高级表达式或同级表达式。
且住,“高级表达式”和“同级表达式”是啥你可能看不懂,因为这是我为了叙述方便发明的新术语。
不过咱不是那种发明术语而不做任何解释管杀不管埋之学风恶劣之人。
我解释一下,高级表达式是指相对某运算符来说,只出现更高优先级运算符的表达式或基本表达式。
例如,对于(二元)+运算符来说,3*5就是高级表达式。
同级表达式是指相对某运算符来说,不出现更低级运算符的表达式。
例如对于(二元)+运算符,2+3就是它的同级表达式,但a=b就不是,因为这里出现了=,=的优先级比+要低。
举例来说,由于(二元)+的优先级低于(二元)*,那么可以对3*6进行+运算:
3*6+2再如由于+和+优先级相同,所以可以对3+6做+运算3+6+2但不可以对3+6做*运算3+6*2虽然合法,但绝对不可能是(3+6)*2的含义。
当把一个运算符添加在高级表达式或同级表达式上是还必须遵守结合性的规定,由于(二元)+运算的结合性是从左到右,所以只能加到高级表达式或同级表达式的右边。
当然还得给它加的另一个操作数,这个操作数必须是高级表达式。
由于(int*)&i不是sizeof的高级表达式或同级表达式,所以希望对它做sizeof运算必须加括号,写成sizeof((int*)&i)。
(注:
((int*)&i)构成了一个基本表达式)
而写成sizeof(int*)&i的话,就如同前面在3+6加上*一样不是(3+6)*2的含义而是3+(6*2)的含义一样,表达的可能是另一种含义,这个含义是
(sizeof(int*)) & i
这里&其实是二元&运算。
既然是&是二元&运算,前面代码中没有给i初值显然不妥,正确的代码是:
viewsourceprint?
#include
#include
intmain(void)
{
inti=3;//whatever
printf("%u",sizeof(int*) );
printf("%u",sizeof(int*)&i);
return0;
}
递归调用函数
C程序结构是函数模块结构,C程序是由一个或多个函数构成的,是函数的集合。
函数具有相对独立的特定功能,是程序的基本单位,因此,在C语言教学中,函数这一章(大部分教材把函数作为一章)是重点内容,而函数的递归调用则是这一章的教学难点之一,在函数递归调用的教学过程中,为了增加学生的兴趣和便于理解,我们通过做游戏的方式引入递归的概念,取得了良好的教学效果。
教学时,教师面对在座的一列学生(假设这列学生有5名),问最后一名学生,即第5名学生,他和他前面这一列学生的年龄总和是多少(假设学生之间不知道相互的年龄),这时第5名学生要知道他和他前面这一列学生的年龄总和,就需先向他前面的第4名学生提相同的问题,第4名同学又需向第3名同学提相同的问题,依次类推,直到最前面的第1名同学。
这时第1名同学前面已无其它同学,这时他只需将自己的年龄告诉后面的第2名同学,第2名同学将前面同学的答案加上自己的年龄然后把结果告诉后面的第3名同学,依次类推,第5同学将第4名同学的答案加上自己的年龄,再告老师,老师就可以知道这列学生年龄的总和。
上述游戏中,求第n个学生和他前面这一列学生年龄总和的功能可用递归函数totalAge(n)来实现,算法可表示为下面的递归公式。
程序如下所示:
#include
inttotalAge(intn)
//用于求第n个学生和他前面这一列学生的年龄总和
{intmyAge;//变量myAge表示第n个学生的年龄
printf("请输入第%d学生的年龄:
",n);
scanf("%d",&myAge);
if(n>1)
returnmyAge+totalAge(n-1);
elseif(n==1)//递归结束条件
returnmyAge;}
voidmain(){
intm;
printf("请输入该列学生的人数:
");
scanf("%d",&m);
if(m<=0)
printf("学生人数不能小于1个人!
\n");
else
printf("该列学生的年龄总和为:
%d\n",totalAge(m));}
通过上面的例子,学生就很容易理解递归的概念,明白递归是由回推和递推两个阶段组成,以及递归为什么必需要有结束条件。
4结束语
在多维数组指针的教学中,通过引入面指针、行指针和列指针,并与相应级别的指针相关联,使教学中许多难点和容易混淆的问题变得易于理解和掌握。
而通过做游戏的方式引入递归的概念,使原来枯燥的教学变得活跃和有趣,学生在游戏中轻松掌握了本来看似深奥和难以理解的知识点。
目前社会上各式各样的计算机等级考试种类繁多,有教育部考试中心组织的全国计算机等级考试,还有各省自行组织的计算机等级考试。
这些计算机等级考试受到了广大考生和用人单位的欢迎,有效地衡量了考生的计算机应用能力水平,为用人单位录用人才提供了很好的依据。
计算机等级考试也越来越火热,报考人数逐年递增。
在众多的计算机等级考试当中,涉及的语种颇多,但是C语言无疑是报考人数最多的语种之一。
无论在什么样的C语言考试当中,都有一些必考的知识点,运算符的优先级关系便是其中之一。
1考试试题举例
例1有以下程序
main()
{
intx=0,y=5,z=3;
while(z-->0&&++x<5)
y=y-1;
printf(“
%d,%d,%d\n”,x,y,z);
}
程序执行后的输出结果是
①3,2,0②3,2,-1③4,3,-1④5,-2,-5
该例题出自于2004年4月份全国计算机等级考试二级C语言单选第23题,该题考查的是3个双目运算符>、<和&&的优先级关系。
2C语言运算符优先级表
C语言共有多达45个运算符,运算符优先级关系如表1所示。
表1C语言运算符优先级
运算符结合性
()[]->.从左到右结合
!
~++--+-*&(type)sizeof从右到左结合
*/%从左到右结合
+-从左到右结合
<<>>从左到右结合
<<=>>=从左到右结合
==!
=从左到右结合
&从左到右结合
^从左到右结合
&&从左到右结合
?
:
从右到左结合
=+=-=*=/=%=&=^=|=<<=>>=从右到左结合
从左到右结合
C语言的45个运算符,按照表中的从上到下的顺序优先级依次降低,表中位于同一行的运算符优先级相同。
其实C语言的运算符可以划分为4类。
第一类是根本不真正运算的运算符,如()、[]、->(取结构体或共用体指针成员)、.(取结构体或共用体成员)。
第二类是单目运算符,指的是参加运算符运算的对象只需要一个,如!
、~、++、--、+(正号)、-(负号)、*(取指针指向的对象)、&(取地址)、(type)(强制类型转换)、sizeof等。
第三类是双目运算符,指的是参加运算符运算的对象必须为两个,如*、/、%、+、-、<<、>>、<、<=、>、>=、==、!
=、&、^、、&&、、,(逗号运算符)等。
第四类是三目运算符,指的是参加运算符运算的对象只需要三个。
C语言仅有一个三目运算符?
:
。
3巧记规则
C语言的这些运算符,如果死记硬背强行记住每个的优先级和结合性是很困难的。
下面给出几条巧记C语言运算符优先级和结合性的规则。
第一条规则:
根本不真正运算的运算符优先级最高,例如()、[]、->(取结构体或共用体指针成员)、.(取结构体或共用体成员)。
这四个运算符实际不作数学意义上的运算。
第二条规则:
单目运算符高于双目和三目运算符,同是单目运算符从右到左计算。
例如,!
、~、++、--、+(正号)、-(负号)、*(取指针指向的对象)、&(取地址)、(type)(强制类型转换)、sizeof。
第三条规则:
双目运算符中算术运算符优先级最高,其他次之。
算术运算符之间遵循数学中的四则运算法则。
双目运算符中除了算术运算符之外,其他运算符的优先级关系是整个C语言运算符优先级关系中最难记忆的。
笔者根据其优先级关系联系古代官场上的某种现象,总结了一个口诀。
先将除了逗号运算符以外剩下的运算符(包括唯一的一个三目运算符)按照他们在C语言中的名称分为以下几种:
移位运算符:
<<、>>
关系运算符:
<、<=、>、>=、==、!
=
逻辑运算符:
&、^、、&&、(&、^、三者并非真正
的逻辑运算符)
条件运算符:
?
:
赋值运算符:
=、+=、-=、*=、/=、%=、&=、^=、=、<<=、>>=
古代的官场存在一种不良的社会风气,人们称之为“跑官要官”。
比如某甲想要升迁(移位运算),他很自然想到了去找关系(关系运算),他找关系也得合乎逻辑(逻辑运算),譬如该找吏部尚书,就不能去找工部尚书。
找到了关系,也要满足条件(条件运算),譬如任期满三年才能提拔,任期才一年就不能提拔。
满足了条件就可以安置(赋值)新位子了。
根据以上总结口诀如下:
想要“移位”→得找“关系”→合乎“逻辑”→满足“条件”→“赋值”位子。
补充说明两点:
第一点:
关系运算符中,==、!
=优先级要低于其他关系运算符,两者之间相同。
第二点:
逻辑运算符中,&、^、、&&、,优先级从左到右依次降低。
第四条规则:
逗号运算符优先级最低,所有单目运算符(包括三目)从右到左,所有双目运算符从左到右。
除了以上4条规则以外,还必须说明一点。
标准的C语言编译器对运算符的解析遵循“最大贪婪”规则。
所谓的“最大贪婪”规则指的是,当C语言编译器对源
程序进行词法分析时,如果运算符的下一个符号还是运算符,并且能和前一个运算符构成一个新的合法运算符时,编译器的词法分析器必须将其解析为两个运算符符号构成的新的运算符(C语言中没有3个符号构成的运算符)。
例如:
x=z+++y;是按照x=z+(++y);来计算;还是按照x=(z++)+y;来计算呢?
根据C编译器的“最大贪婪”规则,就必须按照x=(z++)+y;来计算,而不是按照x=z+(++y);来计算。
4应用举例
下面利用上面的规则解决例1中的问题。
例1有以下程序
main()
{
intx=0,y=5,z=3;
while(z-->0&&++x<5)
y=y-1;
printf(“
%d,%d,%d\n”,x,y,z);
}
①3,2,0②3,2,-1③4,3,-1④5,-2,-5
在表达式z-->0&&++x<5中,根据第二条规则,单目运算