运算符和表达式

运算符概览

变量和常量可以参与各种运算,比如:

int a,b,c;
a=1+1;
b=a+1;
c=a+b;

参与运算的常量和变量称为 操作数(Operand),运算使用的符号称为 运算符(Operator),运算符和操作数所组成的算式称为 表达式(Expression)

C 语言的运算符可以分成以下几类:

  • 算术:
    • +、减-、乘*、除/、求余%、自增++、自减--
  • 关系:
    • 大于>、小于<、等于==、大于等于>=、小于等于<=、不等于!=
  • 逻辑
    • &&、或||、非!
  • 位操作
    • 位与&、位或|、位非~、位异或^、左移<<、右移>>
  • 赋值
    • 简单赋值=、复合运算(+=-=等)、复合位运算(&=|=等)
  • 条件
    • 三目运算符?:
  • 逗号:把若干表达式组成一个新的表达式
  • 指针
    • 取内容*、取地址&
  • 求字节:计算数据所占的字节数 sizeof
  • 特殊:
    • 括号()、下标[]、成员-> .

不同运算符放在一起时,按照一定先后顺序计算。一般来说,只需要记住几个诀窍:

  1. 乘除优先于加减
  2. 逻辑非 > 运算 > 关系 > 逻辑与 > 逻辑或 > 赋值
  3. 赋值、复合运算、复合位运算是除 , 外最低的

另外,有几个很容易出错的优先级问题:

优先级问题 表达式 误认为的结果 实际结果
. 高于 *,可以用 -> 来解决 *p.f (*p).f
p所指对象的f
*(p.f)
p的f所指的对象
[] 高于 * int *ap[] int (*ap)[]
ap是个指向数组的指针
int *(ap[])
ap是个指针的数组
函数()高于* int *fp() int (*fp)()
fp是个函数指针,返回 int
int *(fp())
fp是个函数,返回 int*
==!= 高于位操作 (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

最保险的做法还是勤用括号。

除了优先级外,另一个要关注的就是 左值右值。左值指的是可以放在等号左边被赋值的表达式,而右值指的是可以放在等号右边用于赋值的表达式。目前只有变量可以做左值。

逻辑运算符

逻辑运算符会“偷懒”。比如说下面这段代码:

#include<stdio.h>

int main(void){
  int i=0;
  int j=0;
  if((++i>0)||(++j>0)){
    printf("i=%d\n",i);
    printf("j=%d\n",j);
  }
  return 0;
}
\\输出
i=1
j=0

注意到逻辑或 || 两边的语句中,只有前一个执行了,后一个却没有执行。这是因为逻辑或 || 只要前面一个为真,输出即为真,所以后一个就无需执行了。这个问题在逻辑与 && 中也存在,要注意啊❗。

条件运算符

条件运算符 ?: 是一个三目运算符,也就是它有三个操作数:

max=(a>b)?a:b;
\\等价于
if(a>b) max=a;
else max=b;

条件运算符的运行效率在未优化的情况下比 if-else 高,这是因为它俩对应的汇编程序不同。我们可以用下面这段程序比较速度:

#include<stdio.h>
#include<time.h>   //用到clock()函数
int main() {
  int begintime,endtime;
  int a=0;
  int b=1;
  int max;

  //用条件运算符
  begintime=clock();	//计时开始
  for(register int i = 0;i<100000;i++){
    max=(i&1)?a:b;
  };
  endtime = clock();	//计时结束
  printf("Running Time:%dms\n", endtime-begintime);

  //用if-else
  begintime=clock();	//计时开始
  for(register int i = 0;i<100000;i++){
    if(i&1) max=a;
    else max=b;
  };
  endtime = clock();	//计时结束
  printf("Running Time:%dms\n", endtime-begintime);

  return 0;
}

不过现代编译器已经能自动优化 if-else 语句,有时候 if-else 甚至比 ?: 还要高效。所以也不用过分纠结用哪个,一般来说只有简单的赋值才用 ?:.

另外有些考试很喜欢考条件运算符的优先级,比如说:

a>b?a:c>d?c:d

条件运算符的结合顺序是从右到左,所以右边的先计算:

a>b?a:(c>d?c:d)

自增与自减

自增和自减是非常让人迷惑的玩意。举两个例子:

//例1
int i=0,j=0;
printf("%d\n", i++);
printf("%d\n", ++j);
//++在后,i先取值,再自增
//++在前,j先自增,再取值
//例2
int i=3;
(++i)+(++i)+(++i);
//计算方式一:先计算三次 ++i,i 变为 6,然后三个 6 相加得到 18
//计算方式二:先计算前面的 (++i)+(++i),由于有两次 ++i,i 变为 5,5+5=10,然后 i 再自增变为 6,10+6=16
//两种方式都是对的,取决于用哪种编译器
//gcc 使用的是方式二

位运算

首先要注意的是,位运算的操作对象是 int,如果不是 int,就会转换成 int。我们可以用下面这个例子来说明。

unsigned char c = 0xff;
unsigned int i = ~c;
printf("%#x\n",c);
printf("%#x\n",i);
//输出
0xff
0xffffff00

char 是 8 位的,如果对 0xff 取反,结果应该是 0x00,但由于转换成了 int,所以实际上是对 0x000000ff取反,结果应该是 0xffffff00。

位运算在单片机中还是挺常用的,可以用来设置各种寄存器,比如说:

  • 与:把某些位清0,其他位不变
  • 或:把某些位置1,其他位不变
  • 异或:把某些位取反,其他位不变

另外,可以用 « 或 » 来代替乘 $2^n$ 或 除 $2^n$,这样可以加快运算速度。比如:

257/8 == 256>>3;
456%32 == 456-(456>>4<<4)

还有一个 trick 就是可以用异或,在不引入第三变量的情况下,交换两个变量:

int a=0,b=1;
a=a^b;
b=a^b;
a=a^b;
//等效于
//a=(a^b)^((a^b)^b)
//b=(a^b)^b