第二节 · 数字间的反应 · 初识运算符

在开始之前,我们先来回忆一下,在数学中,我们有哪些数学符号可用?

四则运算有加减乘除,比较数字则有大于、小于、大于等于和小于等于。有时,我们还会用到不等于、等于和小括号等符号。

这些符号组成了数学中的算式。同样的,C++ 也有与之功能一致的符号可以用在表达式中,我们称这些符号叫运算符。

首先介绍一下用于算术运算的运算符。不仅有加减乘除(+、-、*、/),C++ 还额外给我们提供了求余运算符。

求余是什么?通常情况下,我们可以将它看成是除法中的求余数。举个例子:

7 ÷ 2 = 3 ······ 1

7 % 2的结果是1,也就是上述式子中的余数。

需要注意的是,求余运算两边的数字必须是整数。

不仅求余运算符有特殊之处,就连除法运算符也颇具特殊之处——两个整数相除,得到的是一个整数,小数部分会被舍弃:

7 / 2 = 3 (不是 3.5)

C++ 不仅能表达正数和 0,也可以表达负数。负数的写法与数学上的写法类似,在数字前加一个减号(相当于负号)即可:

-233

如果要使用负数作四则运算,一定要记得在负数两边加一对小括号包裹起来,以和减号区分开来:

(-233)*(-666)+(-999)/(-333)-(-123)

另外,有关减法与负号之间的关系,其实以下两种写法产生的效果是一样的:

233 + (-66) = 233 - 66

这不仅在 C++ 中是一致的,在数学上也同样成立。

我们在数学上经常会使用括号改变算式的求值顺序:

{[3 (1 + 2) + 6] (9 - 6)} [12 ( 3 + 4)]

在数学中,我们必须按照运算顺序分别使用小括号、中括号和大括号,但在 C++ 中却不需如此麻烦。我们只需要使用小括号就够了:

((3 * (1 + 2) + 6) * (9 - 6)) * (12 * (3 + 4))

在表达式中,小括号最有用的用途之一,是强行改变原有的运算顺序。当然,小括号不仅仅能做到这些,它的其他用户我们以后再作讨论。

C++ 不仅可以表达算术运算,也可以表达逻辑运算。大于号、小于号和等于号我们是有办法直接输入的,但小于等于、大于等于和不等于又怎么输入呢?

很简单,把大于号、小于号或感叹号与等于号拼接起来即可:

3 >= x    // 3 ≥ x
4 <= y    // 4 ≤ y
1 != z    // 1 ≠ z

另外,在 C++ 中,等于并不是以一个等于号表达的,而是需要连续写两个:

22 == 10 + 12    // 相当于数学中的 22 = 10 + 12,返回的值为 true

为什么要这么麻烦?因为 C++ 中还有一种操作叫做赋值,它是用单一一个等于号表达的。如果不能明确区分它们的含义,等于号将会带有歧义性。

接下来我们再来认识一些专用于逻辑运算的运算符。它们并不能直接对诸如数字、字符乃至更复杂的类型作运算,而只能对布尔值作运算。逻辑运算符有非(not,符号为“!”)、或(or,符号为“||”)和与(and,符号为“&&”)。

它们到底能干什么用呢?举个例子:

18 <= x && x <= 45

这一句的意思是,如果x不小于 18,并且也不大于 45,那么得到的值为真,除此之外的一切情况得到的值均为假。

x <= 18 || x >= 65

这一句的意思是,只要x小于 18 x大于 65,得到的值就为真。如果x既不小于 18 也不大于 65,那就只能为假了。

我们兴许还可以组合一下,表达得稍微复杂点:

3 < x && x < 12 || 18 < x && x < 21 || x == 25

注意,与(&&)的优先级要比或(||)高,所以在这个表达式中,最先计算的是3 < x && x < 1218 < x && x < 21x == 25。这一段的意思便是,如果x在 3 与 12 之间,或是在 18 与 21 之间,或者等于 25,都能使得这个表达式的求值结果为真。

还有一个名为非的符号,它的用途是反转一个布尔值,也就是真变假、假变真:

!(3 < x && x < 12)
3 >= x || x >= 12

以上两个式子的求值结果是一样的——将判断“x 是否在 3 到 12 之间”的结果反转一下,就成了“x 是否不在 3 到 12 之间”的结果,或也可以看成是“x 是否小于等于3,或大于等于 12”的结果。

注意,非(!)符号比与(&&)符号和或(||)符号的优先级都要高,所以诸如以下这样的写法并不是等价的:

!3 < x && x < 12    // 相当于 3 >= x && x < 12
!(3 < x && x < 12)    // 相当于 3 >= x || x >= 12

逻辑运算是相当强大的计算手段,但不能将得到的布尔值利用起来,逻辑运算便毫无意义。其中一种利用布尔值的手段,便是使用三元运算符。

三元运算符有点特殊,因为它是由两个相互独立的运算符组成的整体:

<条件> ? <条件为真时的值> : <条件为假时的值>

有点奇怪,对吧?举个实际的例子感受一下,计算出租车行驶xkm 路要收取的车费:

x <= 3 ? 8 : 8 + 1.5 * (x - 3)

x小于等于 3 km 时,收取的费用便是固定的起步价 8 元;当x大于 3 km 时,收取的费用是以起步价为基础,每多 1 km 额外收取 1.5 元。

这个表达式也同样地表达了刚刚的逻辑:当x小于等于 3 时,这个表达式的值是 8,否则为8 + 1.5 × (x - 3)

另外,三元运算符也是可以嵌套的,用于表达更复杂的计算。随便举个例子,感受一下:

money < 10000 ?
    (likeChocolate ? "Buy chocolate" : "Buy normal sweet")
    :
    (needHouse ? "Buy a house" : "Buy a car")

三元运算符也可以叫成三目运算符。另外,三元运算符默认是从右往左计算的,也就是所谓的右结合。

有时候,我们不仅需要用到四则运算作算术操作,也有可能会用到位运算。位运算,实际上是指操作数字的每一个二进制位。位运算有六种:与(&)、或(|)、异或(^)、非(~)、左移(<<)与右移(>>)。

我们先来回顾一下有关二进制位的知识。二进制如同十进制一样,可以进行四则运算,区别仅仅在于它只有 0 和1 两个数字:

1 + 1 = 10

110 - 11 = 11

11 * 101 = 1111

1100 / 110 = 10

由于二进制数字只有 0 和 1,所以我们不仅能进行算术运算,还可以将两个二进制数的每一个数字逐个比对,作与逻辑运算相似的运算。

我们随便取两个长度一致的二进制数试一试:

a        = 10001001
b        = 01101100
a & b    = 00001000    // 与运算,只有第 5 位数两边都是 1
a | b    = 11101101    // 或运算,只有第 4、7 位数两边都不是 1
~a        = 01110110    // 非运算,a 的每一位 0 变 1,1 变 0
a ^ b    = 11100101    // 异或运算,只有第 4、5、7 位数两边数字一样
                    // (有关异或的概念,请阅读 1.1.3 章)

可以看到,位运算实际上就是对每一个二进制位作逻辑运算。

左移(<<)与右移(>>)进行的并不是逻辑运算,而是将原有的数字以二进制的形式向左或向右移动若干位。举个例子,我们为一个类型为 unsigned short 的数字移位,它的长度为 16 个二进制位:

00000000 00101111 << 6  => 00001011 11000000
           ^    ^               ^     ^
                 47 << 6  == 3008        (右侧填充 6 个 0)
00000000 00101111 << 14 => 11110000 00000000
           ^    ^              ^
                  47 << 14 == 61440    (右侧填充 14 个 0,左侧溢出 2 位)
00000000 00101111 >> 4  => 00000000 00000010
           ^    ^                            ^
                  47 >> 4  == 2        (左侧填充 4 个 0,右侧溢出 4 位)

需要注意的是,移位有可能会造成溢出,也就是将一部分原有的数字“挤”除了所在容器所能存储的最大范围。编译器没有办法也不会去检查你所要移动的位数是否会导致溢出,因此,在涉及移位运算时,一定要考虑清楚有关溢出的问题。

小技巧

由于二进制的一些特殊性质,我们每将数字左移一位,便相当于对数字乘以一次 2;反过来,每将数字右移一位,便相当于给数字整除以一次 2:

​ 13 << 1 == 13 * 2

​ 66 >> 2 == 66 / 4

求除以 2 的次幂的余数也因此可以这么写:

​ 137 % 8 == 137 - 137 >> 3 << 3

看到这里,你也许会对之前所学的cincout抱有疑问,为什么对这两个对象进行“左移”、“右移”,实现的效果却不是刚刚所介绍的数字位移?这些符号或许还能有别的含义?

使得,在这里我想稍稍剧透一下,运算符的行为时可以被我们自定义的,在后续的章节中,我们会就针对此深入学习。对于cincout来讲,他们的“左移”、"右移"被标准库自定义了,变成了“输出”与“输入”。

总结一下,我们认识了以下的运算符:

  • 用于四则运算的 + - * /以及求余用的%

  • 改变运算顺序用的小括号()

  • 用于比较大小的> < >= <= == !=

  • 用于根据逻辑运算结果选择表达式用的三元运算符...?...:....

  • 用于综合逻辑运算结果的&& || !

  • 用于进行位运算的& | ^ ~ << >>

习题

  • 在你的身边寻找一些同时有数值与逻辑关系的事物,尝试用表达式写出来。

Last updated