诗海

我们越谦卑,就离真理越近

0%

C语言中的运算符优先级辨析

最近使用C语言写MIT 6.S081课程的作业,发现自己对于C语言中的类似*&[]->符号的运算优先级和结合顺序问题理解并不清楚,这篇文章就是要彻底的理解这些个符号的运算优先级和代表的意义,保证以后凡是看到这样的符号,就都不会心里发慌。

首先,我们要明确,C语言的符号优先级(Precedence)规定了运算符(Operator)的运算顺序,优先级高的符号会先进行运算,然后才轮到优先级低的符号运算。只有对于相同符号优先级的两个运算符,此时讨论结合顺序才是有意义的。结合顺序(Associativity)规定了(在相同优先级符号的情况下)先计算哪个符号的问题。

在同等优先级(Precedence)的情况下,所有的运算符结合顺序(Associativity)一定相同(都是从左到右或者都是从右到左)

举例说明优先级不同造成的区别:

// 由于优先级 * 高于 + 高于 =
// 因此先进行乘法(*)运算,后进行加法(+)运算,最后进行赋值(=)运算
int x = 7 + 3 * 2;

// 由于优先级 ==, != 高于 &, ^
// 因此对于位测试,需要通过()来实现parenthesized expression,才能得到正确结果
if ((x & MASK) == 0) ...

// 由于优先级 -> 高于 (prefix)++
// 因此表达式等价于++(p->len),增加的是len的值
struct {
int len;
char *str;
} *p;
++p->len;

优先级相同情况下,结合顺序造成的区别:

// 由于+,-的结合顺序是从左到右
// 因此先进行左边的+运算,再进行右边的-运算
// 整个表达式等价于(2 + 3) - 5
int x = 2 + 3 - 5;

// 前缀++,--和*,&同级,属于从右向左结合
// 后缀++,--和function call()同级,属于从左向右结合
int arr[] = {10, 20};
int *p = arr;
switch(...) {
case 1:
// 表达式等价于++(*p), arr[] = {11, 20}, p = &arr[0]
++*p;
case 2:
// 表达式等价于*(++p), arr[] = {10, 21}, p = &arr[1]
*++p = 21;
case 3:
// 表达式等价于*(p++), arr[] = {11, 20}, p = &arr[1]
*p++ = 11;
}

// 注意,只有单个运算符的情况下,
// +保证从左到右的结合顺序,但是不能保证 f1() 先执行, f2() 后执行
int y = f1() + f2();
// 相反,多个同级运算符的情况下,就可以局部保证执行顺序,但f3()可以最先执行,也可以最后执行,还可以在f1(), f2()之间执行
// 表达式等价于(f1() + f2()) + f3()
int z = f1() + f2() + f3();

关于前缀和(Prefix ++)以及后缀和(Suffix ++)的问题,可以参考这篇文章显微镜下的 i++ 与 ++i的内容,简而言之就是:

前缀和 ++i:先将局部变量表中的i值+1,再将i放入操作数栈中

后缀和 i++:先将局部变量表中的i放入操作数栈中,再将局部变量表中的i值+1

这其中,操作数栈中的数值就是后续运算(比如说赋值运算int a = i++;)中要操作的数值,因此我们也可以说:

前缀和 ++i:变量i的值+1,整个表达式(++i)的值(操作数栈中存入的值)是变量i的新值

后缀和 i++:变量i的值+1,整个表达式(i++)的值(操作数栈中存入的值)是变量i的原始值

当然,对于指针类型的变量(比如说int *p),前缀和(++p)和后缀和(p++)每一次操作就不是+1了,而是+sizeof(*p)

例如,(++p)->len先执行p+sizeof(*p)的操作,然后再返回这个新p的len值;(p++)->len则返回的是原始p的len值,然后再执行p+sizeof(*p)的操作

全部的运算符优先级可以在网上搜索C language operator precedence得到,这里仅列出本文关注的若干运算符:

Operator Description Associativity
[] Array subscript left to right
-> Member selection via pointer -
++ Suffix increment -
() Function call -
* Dereference right to left
& Address (of operand) -
++ Prefix increment

注意这里的()表示的含义是函数调用(Function call),它也被认为是一种后缀表达式(Postfix Expression),这不同于我们在数学上常用的添加括号(Parentheses)来改变运算顺序的情况。添加括号来改变运算顺序的情况(Parenthesized Expression)是构造了一种初等表达式(Primary Expression)。将括号内的表达式看作是一个整体,其类型和值与无括号的表达式相同。从某种意义上讲,添加括号(Parenthesized Expression)的这种情况是比函数调用()更高的优先级。

其中,[]->同级,它们和()一样,属于最高优先级的那一类运算符;*&同级,它们属于第二档优先级的运算符。

对于->符号来说,它是一种对于结构体(struct)指针的简便写法,对于结构体指针pp->str等价于(*p).str

由于我们有了运算符的优先级和结合顺序,因此也就可以由此读懂复杂的类型声明代码。其中,所有的类型声明语法都应该从里到外(按照优先级顺序)来读

// 变量f是一个function,这个函数的返回值是一个pointer,这个pointer指向int,参数情况未知
// function call operator () 只表示函数,并未规定函数参数情况
int *f();

// 变量pf是一个pointer,这个指针指向一个函数,这个函数的返回值是int,参数情况未知
int (*pf)();

// 变量daytab是一个pointer,这个指针指向一个数组,这个数组长度为13,数组中存储的元素是int类型
int (*daytab)[13];

// 变量daytab是一个array,这个数组长度为13,数组中存储的元素是pointer,每个pointer指向一个int
int *daytab[13];

// 变量comp是一个function,这个函数返回值是一个pointer,这个pointer指向void(全能指针),参数情况未知
void *comp();

// 变量comp是一个pointer,这个指针指向一个函数,函数的返回值是void,参数情况未知
void (*comp)();

// 变量x是一个function,这个函数返回一个指针,指针指向一个数组,这个数组中的每个元素都是一个指针,而且都是指向返回值为char的某个函数的指针
char (*(*x())[])();

// 变量x是一个array,这个数组长度为3,数组中的每个元素都是一个指针,而且都是指向某个函数的指针。其中这个函数也返回一个指针,这个指针是指向一个长度为5的数组,这个数组中的每个元素类型为char
char (*(*x[3])())[5];

最后简单来谈一下函数指针和数组解引用的问题:

对于函数来说,函数的变量名func等价于这个函数定义时(func(params){定义起始位置....})的起始位置(address of function,这也就是操作系统中寄存器存储的return address),而一个函数的指针&func就是对这个函数的变量名进行取地址操作

对于数组来说,数组的变量名array等价于指向数组第一个元素的一个指针,而获得数组的第i个元素的值其实就是对数组指针进行解引用操作,array[i] = *(array + i)&array[i] = array + i。与++相同,对于指针型的变量,我们的+i操作实际上是+ i * sizeof(*array)