C 语言的运算符非常多,一共有 50 多种,可以分成若干类。
算术运算符
- 算术运算符专门用于算术运算,主要有下面几种。
+
:正值运算符(一元运算符)-
:负值运算符(一元运算符)+
:加法运算符(二元运算符)-
:减法运算符(二元运算符)*
:乘法运算符/
:除法运算符%
:余值运算符
+
,-
+
和-
既可以作为一元运算符,也可以作为二元运算符。所谓“一元运算符”,指的是只需要一个运算数就可以执行。一元运算符-
用来改变一个值的正负号。int x = -12;
- 上面示例中,
-
将12
这个值变成-12
。 - 一元运算符
+
对正负值没有影响,是一个完全可以省略的运算符,但是写了也不会报错。int x = -12; int y = +x;
- 上面示例中,变量
y
的值还是-12
,因为+
不会改变正负值。 - 二元运算符
+
和-
用来完成加法和减法。int x = 4 + 22; int y = 61 - 23;
*
- 运算符
*
用来完成乘法。int num = 5; printf("%i\n", num * num); // 输出 25
- 运算符
/
- 运算符
/
用来完成除法。注意,两个整数相除,得到还是一个整数。float x = 6 / 4; printf("%f\n", x); // 输出 1.000000
- 上面示例中,尽管变量
x
的类型是float
(浮点数),但是6 / 4
得到的结果是1.0
,而不是1.5
。原因就在于 C 语言里面的整数除法是整除,只会返回整数部分,丢弃小数部分。 - 如果希望得到浮点数的结果,两个运算数必须至少有一个浮点数,这时 C 语言就会进行浮点数除法。
float x = 6.0 / 4; // 或者写成 6 / 4.0 printf("%f\n", x); // 输出 1.500000
- 上面示例中,
6.0 / 4
表示进行浮点数除法,得到的结果就是1.5
。 - 下面是另一个例子。
int score = 5; score = (score / 20) * 100;
- 上面的代码,你可能觉得经过运算,
score
会等于25
,但是实际上score
等于0
。这是因为score / 20
是整除,会得到一个整数值0
,所以乘以100
后得到的也是0
。 - 为了得到预想的结果,可以将除数
20
改成20.0
,让整除变成浮点数除法。score = (score / 20.0) * 100;
- 运算符
%
- 运算符
%
表示求模运算,即返回两个整数相除的余值。这个运算符只能用于整数,不能用于浮点数。int x = 6 % 4; // 2
- 负数求模的规则是,结果的正负号由第一个运算数的正负号决定。
11 % -5 // 1 - 11 % -5 // -1 - 11 % 5 // -1
- 上面示例中,第一个运算数的正负号(
11
或-11
)决定了结果的正负号。
- 运算符
- 赋值运算的简写形式
- 如果变量对自身的值进行算术运算,C 语言提供了简写形式,允许将赋值运算符和算术运算符结合成一个运算符。
+=
-=
*=
/=
%=
- 下面是一些例子。
i += 3; // 等同于 i = i + 3 i -= 8; // 等同于 i = i - 8 i *= 9; // 等同于 i = i * 9 i /= 2; // 等同于 i = i / 2 i %= 5; // 等同于 i = i % 5
自增运算符,自减运算符
- C 语言提供两个运算符,对变量自身进行
+ 1
和- 1
的操作。 ++
:自增运算符--
:自减运算符i++; // 等同于 i = i + 1 i--; // 等同于 i = i - 1
- 这两个运算符放在变量的前面或后面,结果是不一样的。
++var
和--var
是先执行自增或自减操作,再返回操作后var
的值;var++
和var--
则是先返回操作前var
的值,再执行自增或自减操作。int i = 42; int j; j = (i++ + 10); // i: 43 // j: 52 j = (++i + 10) // i: 44 // j: 54
- 上面示例中,自增运算符的位置差异,会导致变量
j
得到不同的值。这样的写法很容易出现意料之外的结果,为了消除意外,可以改用下面的写法。/* 写法一 */ j = (i + 10); i++; /* 写法二 */ i++; j = (i + 10);
- 上面示例中,变量
i
的自增运算与返回值是分离的两个步骤,这样就不太会出错,也提高了代码的可读性。
关系运算符
- C 语言用于比较的表达式,称为“关系表达式”(relational expression),里面使用的运算符就称为“关系运算符”(relational operator),主要有下面6个。
>
大于运算符<
小于运算符>=
大于等于运算符<=
小于等于运算符==
相等运算符!=
不相等运算符
- 下面是一些例子。
a == b; a != b; a < b; a > b; a <= b; a >= b;
- 关系表达式通常返回
0
或1
,表示真伪。C 语言中,0
表示伪,所有非零值表示真。比如,20 > 12
返回1
,12 > 20
返回0
。 - 关系表达式常用于
if
或while
结构。if (x == 3) { printf("x is 3.\n"); }
- 注意,相等运算符
==
与赋值运算符=
是两个不一样的运算符,不要混淆。有时候,可能会不小心写出下面的代码,它可以运行,但很容易出现意料之外的结果。if (x = 3) ...
- 上面示例中,原意是
x == 3
,但是不小心写成x = 3
。这个式子表示对变量x
赋值3
,它的返回值为3
,所以if
判断总是为真。 - 为了防止出现这种错误,有的程序员喜欢将变量写在等号的右边。
if (3 == x) ...
- 这样的话,如果把
==
误写成=
,编译器就会报错。/* 报错 */ if (3 = x) ...
- 另一个需要避免的错误是,多个关系运算符不宜连用。
i < j < k
- 上面示例中,连续使用两个小于运算符。这是合法表达式,不会报错,但是通常达不到想要的结果,即不是保证变量
j
的值在i
和k
之间。因为表示运算符是从左到右计算,所以实际执行的是下面的表达式。(i < j) < k
- 上面式子中,
i < j
返回0
或1
,所以最终是0
或1
与变量k
进行比较。如果想要判断变量j
的值是否在i
和k
之间,应该使用下面的写法。i < j && j < k
逻辑运算符
- 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,主要有下面三个运算符。
!
:否运算符(改变单个表达式的真伪)。&&
:与运算符(两侧的表达式都为真,则为真,否则为伪)。||
:或运算符(两侧至少有一个表达式为真,则为真,否则为伪)。
- 下面是与运算符的例子。
if (x < 10 && y > 20) printf("Doing something!\n");
- 上面示例中,只有
x < 10
和y > 20
同时为真,x < 10 && y > 20
才会为真。 - 下面是否运算符的例子。
if (!(x < 12)) printf("x is not less than 12\n");
- 上面示例中,由于否运算符
!
具有比<
更高的优先级,所以必须使用括号,才能对表达式x < 12
进行否运算。当然,合理的写法是if (x >= 12)
,这里只是为了举例。 - 对于逻辑运算符来说,任何非零值都表示真,零值表示伪。比如,
5 || 0
会返回1
,5 && 0
会返回0
。 - 逻辑运算符还有一个特点,它总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是保证的。如果左边的表达式满足逻辑运算符的条件,就不再对右边的表达式求值。这种情况称为“短路”。
if (number != 0 && 12/number == 2)
- 上面示例中,如果
&&
左侧的表达式(number != 0
)为伪,即number
等于0
时,右侧的表达式(12/number == 2
)是不会执行的。因为这时左侧表达式返回0
,整个&&
表达式肯定为伪,就直接返回0
,不再执行右侧的表达式了。 - 由于逻辑运算符的执行顺序是先左后右,所以下面的代码是有问题的。
while ((x++ < 10) && (x + y < 20))
- 上面示例中,执行左侧表达式后,变量
x
的值就已经变了。等到执行右侧表达式的时候,是用新的值在计算,这通常不是原始意图。
位运算符
C 语言提供一些位运算符,用来操作二进制位(bit)。
- 取反运算符
~
- 取反运算符
~
是一个一元运算符,用来将每一个二进制位变成相反值,即0
变成1
,1
变成0
。// 返回 01101100 ~ 10010011
- 上面示例中,
~
对每个二进制位取反,就得到了一个新的值。 - 注意,
~
运算符不会改变变量的值,只是返回一个新的值。
- 取反运算符
- 与运算符
&
- 与运算符
&
将两个值的每一个二进制位进行比较,返回一个新的值。当两个二进制位都为1
,就返回1
,否则返回0
。// 返回 00010001 10010011 & 00111101
- 上面示例中,两个八位二进制数进行逐位比较,返回一个新的值。
- 与运算符
&
可以与赋值运算符=
结合,简写成&=
。int val = 3; val = val & 0377; // 简写成 val &= 0377;
- 与运算符
- 或运算符
|
- 或运算符
|
将两个值的每一个二进制位进行比较,返回一个新的值。两个二进制位只要有一个为1
(包含两个都为1
的情况),就返回1
,否则返回0
。// 返回 10111111 10010011 | 00111101
- 或运算符
|
可以与赋值运算符=
结合,简写成|=
。int val = 3; val = val | 0377; // 简写为 val |= 0377;
- 或运算符
- 异或运算符
^
- 异或运算符
^
将两个值的每一个二进制位进行比较,返回一个新的值。两个二进制位有且仅有一个为1
,就返回1
,否则返回0
。// 返回 10101110 10010011 ^ 00111101
- 异或运算符
^
可以与赋值运算符=
结合,简写成^=
。int val = 3; val = val ^ 0377; // 简写为 val ^= 0377;
- 异或运算符
- 左移运算符
<<
- 左移运算符
<<
将左侧运算数的每一位,向左移动指定的位数,尾部空出来的位置使用0
填充。// 1000101000 10001010 << 2
- 左移运算符
- 上面示例中,
10001010
的每一个二进制位,都向左侧移动了两位。 - 左移运算符相当于将运算数乘以2的指定次方,比如左移2位相当于乘以4(2的2次方)。
- 左移运算符
<<
可以与赋值运算符=
结合,简写成<<=
。int val = 1; val = val << 2; // 简写为 val <<= 2;
- (6)右移运算符
>>
- 右移运算符
>>
将左侧运算数的每一位,向右移动指定的位数,尾部无法容纳的值将丢弃,头部空出来的位置使用0
填充。// 返回 00100010 10001010 >> 2
- 上面示例中,
10001010
的每一个二进制位,都向右移动两位。最低的两位10
被丢弃,头部多出来的两位补0
,所以最后得到00100010
。 - 注意,右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。
- 右移运算符相当于将运算数除以2的指定次方,比如右移2位就相当于除以4(2的2次方)。
- 右移运算符
>>
可以与赋值运算符=
结合,简写成>>=
。int val = 1; val = val >> 2; // 简写为 val >>= 2;
- 右移运算符
逗号运算符
- 逗号运算符用于将多个表达式写在一起,从左到右依次运行每个表达式。
x = 10, y = 20;
- 上面示例中,有两个表达式(
x = 10
和y = 20
),逗号使得它们可以放在同一条语句里面。 - 逗号运算符返回最后一个表达式的值,作为整个语句的值。
int x; x = 1, 2, 3;
- 上面示例中,逗号的优先级低于赋值运算符,所以先执行赋值运算,再执行逗号运算,变量
x
等于1
。
运算优先级
- 优先级指的是,如果一个表达式包含多个运算符,哪个运算符应该优先执行。各种运算符的优先级是不一样的。
3 + 4 * 5;
- 上面示例中,表达式
3 + 4 * 5
里面既有加法运算符(+
),又有乘法运算符(*
)。由于乘法的优先级高于加法,所以会先计算4 * 5
,而不是先计算3 + 4
。 - 如果两个运算符优先级相同,则根据运算符是左结合,还是右结合,决定执行顺序。大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行),比如赋值运算符(
=
)。5 * 6 / 2;
- 上面示例中,
*
和/
的优先级相同,它们都是左结合运算符,所以从左到右执行,先计算5 * 6
,再计算6 / 2
。 - 运算符的优先级顺序很复杂。下面是部分运算符的优先级顺序(按照优先级从高到低排列)。
- 圆括号(
()
) - 自增运算符(
++
),自减运算符(--
) - 一元运算符(
+
和-
) - 乘法(
*
),除法(/
) - 加法(
+
),减法(-
) - 关系运算符(
<
、>
等) - 赋值运算符(
=
)
- 圆括号(
- 由于圆括号的优先级最高,可以使用它改变其他运算符的优先级。
int x = (3 + 4) * 5;
- 上面示例中,由于添加了圆括号,加法会先于乘法进行运算。
- 完全记住所有运算符的优先级没有必要,解决方法是多用圆括号,防止出现意料之外的情况,也有利于提高代码的可读性。
下一节:C 语言的程序是顺序执行,即先执行前面的语句,再执行后面的语句。开发者如果想要控制程序执行的流程,就必须使用流程控制的语法结构,主要是条件执行和循环执行。