重学C语言:运算符与优先级

C 语言有很多种运算符(差不多50种),如果一条表达式中有多个运算符,就不可避免的产生二义性.C 语言通过优先级与结合性来解决这个问题,因运算符过多,优先级难以全部准确记忆,故将运算符优先级表记录在此,以备日后有疑问时进行查阅.

C 运算符优先级

运算符的种类

常用的运算符有如下几类:

  1. 算数运算符(+ - * / % ++ --
  2. 关系运算符(== != > < >= <=
  3. 逻辑运算符(&& || !
  4. 位运算符(& | ^
  5. 赋值运算符(= += -+ *= /= %= <<= >>= &= ^= |=
  6. 杂项运算符(sizeof() & * ?:

算数运算符

+-既可以是一元运算符正负号,又可以是二元运算符加减.一元运算符优先级要高于二元运算符.

1
-i + j / k; // 等价于 (-i) + ( j / k)

当表达式中包含两个或者更多个相同优先级的运算符时,就需要根据结合性的规则来运算.

如果运算符是从左向右结合的,就称为左结合,如果是从右像左结合的就称为右结合.二元算数运算符都是左结合的.

1
2
i - j - k; // 等价于 (i - j) - k
i * j / k; // 等价于 (i * j) / k

一元算数运算符都是右结合的.

1
- + i; // 等价于 -(+i)

赋值运算符

在许多编程语言中,赋值是语句.然而,在C语言中,赋值就像 + 一样是运算符.换句话说,赋值操作产生结果,就如同两数相加产生结果一样.赋值表达式 v=e 的值就是赋值运算后 v 的值.

既然赋值是运算符,那么多个赋值就可以串联在一起:

1
i = j = k = 0;

运算符 = 是右结合的,所以上述表达式等价于:

1
i = (j = (k = 0));

有一个例子来说明赋值表达式:

1
2
3
4
5
6
7
8
9
char *strcat (char *s1, const char *s2)
{
char *p = s1;
while (*p)
p++;
while (*p++ = *s2++)
;
return s1;
}

这是个字符串拼接函数,函数中的两个 while 循环的条件表达式都是赋值表达式,因此除了空字符以外所有的字符都为真,当指针到达字符串末尾的 '\0' 时,表达式的值为 0.

自增运算符和自减运算符

自增自减运算符略微有些复杂,那是因为它们既可以作为前缀又可以作为后缀,而且它们还有个副作用就是:会改变操作数的值.

因此,使用自增自减运算符时,我们要谨记两点:

  1. 表达式的值
  2. 操作数的值
1
2
3
4
5
6
7
8
int i = 1;
printf("i is %d\n", i++); // prints "i is 1"
printf("i is %d\n", i); // prints "i is 2"

int i = 1;
printf("i is %d\n", ++i); // prints "i is 2"
printf("i is %d\n", i); // prints "i is 2"

i++ 先赋值再自增,++i 先自增再赋值.

需要注意的是,后缀运算符要比前缀运算符优先级要高,从文章开头的表格中可以看到,后缀自增自减优先级为 1,前缀自增自减优先级为 2.

试想一下,对于 *p++; 语句,自增的是指针还是指针指向的值呢?

很多地方包括 K&R 都是这样解释:尽管 * 和 ++ 的优先级相同,但由于连接的规则是从右往左,所以 p 先和 ++ 进行连接.因此,被自增的不是 *p 而是 p.

对照开头的优先级表,后缀 ++ 优先级要比间接寻址运算符 * 高,因此被进行加法的不是 *p 而是 p.

从语法上看,这种解释才比较合理.

表达式语句

C 语言有一条不同寻常的规则,那就是任何表达式都可以用作语句.换句话说,不论表达式是什么类型,计算什么结果,我们都可以通过在后面添加分号将其转化为语句.例如,可以把表达式 ++i 转换为语句

1
i++;

i 先自增再赋值,但是值会被丢弃,接着执行下一条语句.

一个优先级引起的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void str_cli(FILE *fp, int sockfd)
{
ssize_t n;
char sendline[MAXLINE];
char recieve[MAXLINE];

while (fgets(sendline, MAXLINE, fp) != NULL)
{
// 消除字符串中的换行符
int i = strlen(sendline);
if (sendline[i - 1] == '\n')
{
sendline[i - 1] = 0;
}

Writen(sockfd, sendline, strlen(sendline));

if ((n = read(sockfd, recieve, MAXLINE) == 0))
err_quit("str_cli: server terminated prematurely");

recieve[n] = 0;
Fputs(recieve, stdout);
Fputs("\n", stdout);
}
}

这是我编写的一个 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 被置空.