6.4 对象和值
我们可以分配并使用没有名字的对象(比如用new创建对象),也能为某些看起来不太寻常的表达式赋值(如,*p[a+10] = 7)。因此,我们需要用一个名字来表示“内存中的某个东西”。这就是对象一词最简单和最基本的含义。换句话说,对象(object)是指一块连续存储区域,左值(lvalue)是指向对象的一条表达式。“左值”的字面意思是“能用在赋值运算符左侧的东西”,但其实不是所有左值都能用在赋值运算符左侧,左值也有可能指向某个常量(见7.7节)。未被声明成const的左值称为可修改的左值(modifiable lvalue)。此处我们提到的对象的最简单和最基本的含义不应该与类的对象或多态类型的对象混淆(见3.2.2节和20.3.2节)。
左值和右值
为了补充和完善左值的含义,我们相应地定义了右值(rvalue)。简单来说,右值是指“不能作为左值的值”,比如像函数返回值一样的临时值。
如果你希望技术性更强一些(比如你想读一下ISO C++标准),那就需要更新看待左值和右值的视角了。当考虑对象的寻址、拷贝、移动等操作时,有两种属性非常关键。
- 有身份(Has identity):在程序中有对象的名字,或指向该对象的指针,或该对象的引用,这样我们就能判断两个对象是否相等或者对象的值是否发生了改变。
- 可移动(Is movable):能把对象的内容移动出来(比如,我们能把它的值移动到其他某处,剩下的对象处于合法但未指定的状态,与拷贝是有差别的,见17.5节)。
在上述两个属性的四种组合形式中,有三种需要用C++语言规则精确地描述(既没有身份又不能移动的对象不重要)。我们“用m表示可移动”,且“用i表示有身份”,从而把表达式的分类表示成下图所示的形式:
从图中可知,一个经典的左值有身份但不能移动(因为我们可能会在移动后仍然使用它),而一个经典的右值是允许执行移出操作的对象。其他一些有关的术语还包括纯右值(prvalue)、泛左值(glvalue)和特别值(xvalue,又称为专家值,人们对于这个“x”的解释极具想象力)。例如:
void f(vector<string>& vs)
{
vector<string>& v2 = std::move(vs); //移动vs到v2
//...
}
此处,std::move(vs)是一个特别值。它明显有身份(我们能像vs一样引用它),并且我们显式地给予了将其值移出的许可,方式是调用std::move()(见3.2.2节和35.5.1节)。
在实际编译过程中,考虑左值和右值就足够了。一条表达式要么是左值,要么是右值,不可能两者都是。
对象的生命周期
对象的生命周期(lifetime)从对象的构造函数完成的那一刻开始,直到析构函数执行为止。对于那些没有声明构造函数的类型(比如int),我们可以认为它们拥有默认的构造函数和析构函数,并且这两个函数不执行任何实际操作。
我们从生命周期的角度把对象划分成以下类别:
- 自动(automatic)对象:除非程序员特别说明(见12.1.8节和16.2.12节),否则在函数中声明的对象在其定义处被创建,当超出作用域范围时被销毁。这样的对象被称为自动(automatic)对象。在大多数实现中,自动对象被分配在栈空间上。每调用一次函数,获取新的栈帧(stack frame)以存放它的自动对象。
- 静态(static)对象:在全局作用域或名字空间作用域(见6.3.4节)中声明的对象以及在函数(见12.1.8节)或类(见16.2.12节)中声明的static成员只被创建并初始化一次,并且直到程序结束之前都“活着”(见15.4.3节)。这样的对象被称为静态对象可能会造成某些意料之外的问题。因为所有线程都共享静态对象,所以必须为其加锁以避免数据竞争(见5.3.1节和42.3节)。
- 自由存储(free store)对象:用new和delete直接控制其生命周期的对象。
- 临时(temporary)对象:比如计算的中间结果或用于存放const实参引用的值的对象。临时对象的生命周期由其用法决定。如果临时对象被绑定到一个引用上,则它的生命周期就是引用的生命周期;否则,临时对象的生命周期与它所处的完整表达式一致。其中,完整表达式(full expression)不属于任何其他表达式。通常情况下,临时对象也是自动对象。
- 线程局部(thread-local)对象,或者说声明为thread-local(见42.2.8节)的对象:这样的对象随着线程的创建而创建,随着线程的销毁而销毁。
其中,静态和自动被称为存储类(storage class)。
数组元素和非静态类成员的生命周期由它们所属的对象决定。