C语言中的运算符绝对是C语言学习和使用的一个难点,因为在2011版的标准中,C语言的运算符的数量超过40个,甚至比关键字的数量还要多。这些运算符有单目运算符、双目运算符以及三目运算符,又涉及到左结合和右结合的问题,真是令人眼花缭乱。
1、运算符及优先级
运算符多可能使用更灵活方便,但这还涉及到运算符之间的优先级问题。我们做四则运算式时,有先乘除后加减的规定,在C语言的这些运算符中自然也是有的,但40多个运算符排起优先级来,使用就不那么容易了。
接下来我们简单的总结一下C语言中运算符以及他们的优先级。C语言中各运算符的优先级如下表所示:
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 |
|
() | 圆括号 | (表达式)/函数名(形参表) |
| ||
. | 成员选择(对象) | 对象.成员名 |
| ||
-> | 成员选择(指针) | 对象指针->成员名 |
| ||
++ | 后置自增运算符 | ++变量名 | 单目运算符 | ||
-- | 后置自减运算符 | --变量名 | 单目运算符 | ||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 |
| ||
++ | 前置自增运算符 | 变量名++ | 单目运算符 | ||
-- | 前置自减运算符 | 变量名-- | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) |
| ||
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式/整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 |
|
/= | 除后赋值 | 变量/=表达式 |
| ||
*= | 乘后赋值 | 变量*=表达式 |
| ||
%= | 取模后赋值 | 变量%=表达式 |
| ||
+= | 加后赋值 | 变量+=表达式 |
| ||
-= | 减后赋值 | 变量-=表达式 |
| ||
<<= | 左移后赋值 | 变量<<=表达式 |
| ||
>>= | 右移后赋值 | 变量>>=表达式 |
| ||
&= | 按位与后赋值 | 变量&=表达式 |
| ||
^= | 按位异或后赋值 | 变量^=表达式 |
| ||
|= | 按位或后赋值 | 变量|=表达式 |
| ||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 | 从左向右顺序运算 |
在上表中,我们归纳了运算符、个运算符的功能、通常的应用表达式形式以及结合性。说到结合性主要应用于相同优先级的运算符,运算次序由结合方向所决定。绝大部分的运算符都是左结合的,与我们的常识一致。不过有一部分运算符是右结合的,这些就需要记忆了,但记忆有时候却不见得百分百准确,所以我们可以在编写代码时以( )加以规范。
2、优先级的一些特别说明
我们见面归纳了运算符的用法,但这只是一般性情况,实际的使用情况有时候依然让人迷惑。比方说,在上表中,如果优先级同为1 的几种运算符同时出现时,想要确定表达式的优先级就不是那么明显了。例如我们常常对指向多维数组的指针和指针数组,还有指向函数的指针数组等含混不清,对初学者来说就更是迷惑不解。所以在这样的定义表达式中,如果优先级不明确结果有可能相去甚远。在这里,接下来我们整理了一些常常容易出错的情况说明如下:
优先级问题 | 表达式 | 想要的结果 | 实际结果 |
.的优先级高于* | *p.f | 指针p所指向的对象的某一字段f,即:(*p).f | 对p取f偏移作为指针,然后进行解除饮用操作,即:*(p.f) |
[]的优先级高于* | int *p[] | P是指向int数组的指针,即:int (*p)[] | P是元素为指向int的指针的数组,即:int *(p[]) |
函数()优先级高于* | int *p() | p是个函数指针所指函数返回值是int,即:int (*p)() | P是个函数,返回值是int *,即:int *(p()) |
==和!=优先级高于位操作 | (val&mask!=0) | (val&mask)!=0 | val&(mask!=0) |
==和!=优先级高于赋值操作 | c=getchar()!=EOF | (c=getchar())!=EOF | c=(getchar()!=EOF) |
算术运算符高于移位运算符 | msb<<4+lsb | (msb<<4)+lsb | msb<<(4+lsb) |
逗号运算符的优先级最低 | i=1,2 | i=(1,2) | (i=1),2 |
那么是不是有了上面的总结就完事大吉了呢?当然不可能,且不说我们的总结很不完备,就算你记住了所有的规则,在使用过程中仍然有可能漏洞百出,最好的办法是养成良好的编码习惯。
3、由优先级引发的错误
优先级问题经常会造成一些意想不到的结果,有的甚至是非常严重的。本人在编写程序时,就出现过一些因为疏忽运算符优先级造成的问题,而且检查起来非常不容易发现。
例如,有一次,我们采集了传感器的原始数据,然后会对数据进行一些处理,在其中的一种条件下会对一个数进行左移几位并加上一个数。由于算术运算符的优先级大于移位运算符,而程序员忘记了给移位操作加括号,所以得出了结果总是有误,只好从头开始查找,花了不少时间才发现这出错误。如msb<<4+lsb和(msb<<4)+lsb是完全不一样,因为算术运算符的优先级大于移位运算符。
还有一次,我们定义一个指向函数的指针数组用于回调函数的操作。定义时,没有考虑到指针运算符、函数运算符以及数组运算符的优先级问题而造成调用出错。如,void (*p[])f()的形式,如果写成void *p[]f()的形式就完全错误了。
当然,如果能够熟记各种运算符的优先级也许能解决上面的问题,但这实际上却很难达到,因为应用是非常灵活的,你不可能时时刻刻只关注于此。我觉得养成良好的编码习惯以及多多练习似乎更有效。
欢迎关注: