第三节 · 传送带 · 赋值操作

我们时常让计算机作出各种各样的计算,但如果不能将计算结果截留下来,先前的劳动便成了昙花一现,变得没有了意义。

也许我们一时还可以用cout,将算得的结果立即变为屏幕上的黑底白字:

cout << 1 + 1;

也许我们还可以用cin和变量。变量的存在给予了我们截留运算结果的可能,但目前为止,我们还仅仅在让它承载从键盘获得的值:

int x, y, z;
cin >> x >> y >> z;
cout << x * y * z;

接下来,我们便学习如何为变量赋予真正接纳数据的能力。

变量可以被赋予一个值,这种操作便叫作赋值:

int x, y, z;
x = 1;
y = 2 + 3;
z = x + 2 * y;

赋值时使用单一的一个等于号,左侧为被赋值的对象,右侧为用于赋值的值。左侧的可持久存储的对象所含的值称为左值,右侧的赋值之后结果就会被丢弃的值称为右值。

分左、右值的目的何在?其实这是想为程序员传达这样一种讯息:左值(例如变量、类的成员)不会凭空小时,它就在那里,你总有机会将其值取走;右值(例如一段表达式)就真的是一闪即逝,不论你是否将这个值存进某个左值所在的容器,或是将其作为某一段在它之上的表达式的一部分继续计算,求完这个值,它便会消失。赋值的目的,也本就是将如过客般的右值,变为原本左值所在容器的新主人。

看起来好复杂的样子。

不必忧虑,我们仍然可以继续接下来的学习,因为左、右值的性质直到靠后的章节才会用到。目前为止,我们仅需了解过左、右值,并且能做到使用一个单一的等于号赋值,就足够了。

赋值可以有什么新花样么?

答案是,有!下面来看一看混杂了各种运算符的奇怪赋值方法:

a += 1;
b -= 5;
c *= a + b + c;

这些是什么玩意?为什么等于号和普通符号粘在一起了?

其实,它们与以下表达式是等价的:

a = a + 1;
b = b - 5;
c = c * (a + b + c);

如果你是第一次看到这种表达式,不由得会觉得不合情理。毕竟,像a = a + 1这种表达式在数学上根本不可能成立!

等一等,我们之前有了解过,两个等号才是比较相等,而一个等号的含义是赋值。

也就是说,这个表达式其实并没有什么毛病,只不过是有些违背常规思维罢了。

为了理解这个式子到底是怎么计算的,我们需要站在计算机的角度,具体地看每个步骤:

  1. 取出a,我们先假设a原本等于 2 吧。

  2. 计算a + 1,也就是 2 + 1 = 3。

  3. 将算得的 3 存进a,此时a便从原本的 2 变成了 3。

噢!这不就是让a自己加一嘛!

照此推下去,我们也可以得出,b = b - 5是指让b自己减去 5,c = c * (a + b + c)是让c自己乘上a + b + c的和。

刚开头的像a += 1这样的式子,便是像a = a + 1这样的式子的简写形式。如同+=这种将其它二元运算符与赋值用的等于号拼起来是想传达一种理念:它左侧的变量不仅是此次运算的参与者,并且同时也是运算对象的承载者。这样的操作称为复合赋值

我们经常会遇到要让变量自增或自减 1 的情形。

例如,游戏到了新的一回合,那就:

round += 1;

打怪升了一级,那就:

level += 1;

游戏角色阵亡,扣除一次复活机会,那就:

chances -= 1;

也许你会觉得,这样写也差不多够用了诶,起码比round = round + 1这种的好多了。

不过,还有更简便的写法,它们被称作递增递减

round++;    // 或 ++round
level++;    // 或 ++level
chances--;    // 或 --chances

得,这下连等于号都不用写了。

看起来是挺简单的——把+=-=改为++--就好了。不过,与复合赋值不同的是,这个++--是既可写在变量之前,也可写在变量之后的。

这么做有区别么?有!简而言之,区别在于是先取值后赋值,还是先赋值后取值。

我们写个程序试一试:

#include <iostream>
using namespace std;
int main()
{
    int n = 233, m = 233;
    cout << ++n << m++ << endl;
    cout << n << m << endl;
    return 0;
}

运行时的输出是这样的:

234 233

234 233

前一个n似乎合情合理,输出的是加过一后的值,但后一个m却还没加就把数字打出来了。不过,从第二行cout的输出结果可以看出,它们都自己加了一,工作的挺正常。

我们可以从中看出,如果递增(减)运算符是写在某变量之前的(称作前置运算符后缀运算符),那么递增(减)将立即执行,你所收集到的值也会是加(减)过后的样子;如果写在某变量之后(称作后置运算符后缀运算符),这个变量的递增(减)操作虽然也会立即执行,但执行过后你取得的并不是加(减)过后的值,而是该变量原本的值复制得到的一份副本。当我们将递增(减)用在了复合表达式时,便需要额外注意这些问题了。除非必要,我们一般使用前置版本的运算符,以避免不必要的复制造成的性能损失。

也许有聪明的读者,在思考能不能把值赋给一个右值,比如说数字之类的。

很遗憾,这种操作是不合法的——右值并没有储存其值的空间,哪怕你真的把值给了它,它也没地方存,只能是丢弃:

233 = 666;        // 错误,233 是个字面量,属于右值
1.5 = 3.4;        // 错误,1.5 是个字面量,也属于右值

另外,你也无法为一个常量赋值——常量是不可变的量,怎么能容许被赋值改变呢?

constexpr auto a = 233;
const auto b = a * 3;
a = 666;    // 错误,constexpr 下的是常量中的常量,编译时就定型了,根本改不了
b = 666;    // 错误,带有 const 意味着你不得改变它的值

现在我们回顾一下:

  • 赋值,用于改变一个变量的值,如a = 1 + b

  • 复合赋值,可以用在自增或自减的情况,如a += 3,它与a = a + 3是等价的。

  • 递增与递减,是复合赋值的特殊情况,为一个变量自行加一或减一,如a++++a均和a += 1等价。它既可写在变量前,也可写在变量后,分别称作前置运算符(前缀运算符)与后置运算符(后缀运算符),这将会决定它赋值与取值的先后顺序。

  • 列表初始化等高级些的初始化方法,也适用于赋值。

  • 不能为一个右值或常量赋值。

习题

  • 不以计算机运行,直接口述下述程序的运行结果,然后再以计算机验证你的答案:

    int n = 3;
    cout << n++ << endl;
    cout << ++n << endl;

Last updated