重学C语言:运算符与优先级
C 语言有很多种运算符(差不多50种),如果一条表达式中有多个运算符,就不可避免的产生二义性.C 语言通过优先级与结合性来解决这个问题,因运算符过多,优先级难以全部准确记忆,故将运算符优先级表记录在此,以备日后有疑问时进行查阅.
运算符的种类
常用的运算符有如下几类:
- 算数运算符(
+ - * / % ++ --
) - 关系运算符(
== != > < >= <=
) - 逻辑运算符(
&& || !
) - 位运算符(
& | ^
) - 赋值运算符(
= += -+ *= /= %= <<= >>= &= ^= |=
) - 杂项运算符(
sizeof() & * ?:
)
算数运算符
+
和-
既可以是一元运算符正负号,又可以是二元运算符加减.一元运算符优先级要高于二元运算符.
1 | -i + j / k; // 等价于 (-i) + ( j / k) |
当表达式中包含两个或者更多个相同优先级的运算符时,就需要根据结合性
的规则来运算.
如果运算符是从左向右结合的,就称为左结合
,如果是从右像左结合的就称为右结合
.二元算数运算符都是左结合的.
1 | i - j - k; // 等价于 (i - j) - k |
一元算数运算符都是右结合的.
1 | - + i; // 等价于 -(+i) |
赋值运算符
在许多编程语言中,赋值是语句
.然而,在C语言中,赋值就像 +
一样是运算符
.换句话说,赋值操作产生结果,就如同两数相加产生结果一样.赋值表达式 v=e
的值就是赋值运算后 v 的值.
既然赋值是运算符,那么多个赋值就可以串联在一起:
1 | i = j = k = 0; |
运算符 =
是右结合的,所以上述表达式等价于:
1 | i = (j = (k = 0)); |
有一个例子来说明赋值表达式:
1 | char *strcat (char *s1, const char *s2) |
这是个字符串拼接函数,函数中的两个 while 循环的条件表达式都是赋值表达式,因此除了空字符
以外所有的字符都为真,当指针到达字符串末尾的 '\0'
时,表达式的值为 0
.
自增运算符和自减运算符
自增自减运算符略微有些复杂,那是因为它们既可以作为前缀又可以作为后缀,而且它们还有个副作用就是:会改变操作数的值
.
因此,使用自增自减运算符时,我们要谨记两点:
- 表达式的值
- 操作数的值
1 | int i = 1; |
i++
先赋值再自增,++i
先自增再赋值.
需要注意的是,后缀运算符要比前缀运算符优先级要高,从文章开头的表格中可以看到,后缀自增自减优先级为 1,前缀自增自减优先级为 2.
试想一下,对于 *p++;
语句,自增的是指针还是指针指向的值呢?
很多地方包括 K&R 都是这样解释:尽管 * 和 ++ 的优先级相同,但由于连接的规则是从右往左,所以 p 先和 ++ 进行连接.因此,被自增的不是 *p 而是 p.
对照开头的优先级表,后缀 ++ 优先级要比间接寻址运算符 * 高,因此被进行加法的不是 *p 而是 p.
从语法上看,这种解释才比较合理.
表达式语句
C 语言有一条不同寻常的规则,那就是任何表达式都可以用作语句.换句话说,不论表达式是什么类型,计算什么结果,我们都可以通过在后面添加分号将其转化为语句.例如,可以把表达式 ++i 转换为语句
1 | i++; |
i 先自增再赋值,但是值会被丢弃,接着执行下一条语句.
一个优先级引起的错误
1 | void str_cli(FILE *fp, int sockfd) |
这是我编写的一个 socket 客户端的代码,我在读取 socket 描述符中的内容时犯了错误,在 if 条件中的括号放错了位置,这导致我原本想要进行:
1 | if ((n = read(sockfd, recieve, MAXLINE)) == 0) |
变成了:
1 | if ((n = read(sockfd, recieve, MAXLINE) == 0)) |
也就是条件变成了 n = read() == 0
,对照优先级表,关系运算符 ==
要比赋值运算符 =
优先级高,所以这里先进行 read 读取,再与0进行判等,当有正常内容被读出的时候,结果为假,之后赋值给 n,这里 n 会变成 0,进而造成 recieve 被置空.