一,左值与右值
1,左值与左值引用
左值是一个表示数据的表达式,程序可以获取其地址。左值可以出现在赋值语句的左边,也可以出现在赋值语句的右边。左值引用就是对左值的引用。下面的变量都是左值
int a = 20;
const int b = 10;
int c = b;
2,右值与右值引用
右值即可出现在赋值表达式右边,但不能获取其地址。右值包括字面常量(C风格字符串除外,它表示的是地址),诸如x + y表达式以及返回值的函数(条件是该函数返回的不是引用)。C++11新增了右值引用,这是使用&&表示的,右值引用就是对右值的引用。下面的是右值的例子
int getValue(){
return 50;
}
int main(){
int x = 10;
int y = 20;
int &&r1 = 30; //字面常量是右值
int &&r2 = x + y; //表达式是右值
int &&r3 = getValue(); //函数返回的是int类型的值
return 0;
}
二,为何需要移动语义?
我们定义了下面这个Useless类
class Useless{
private:
string name;
char *p;
public:
Useless();
Useless(string name);
Useless(const Useless &f);
Useless operator+(const Useless &f) const;
void showData();
~Useless();
};
定义一个函数这个函数的返回值是Useless类对象
Useless getObject(){
Useless temp("temp");
return temp;
}
假设有下面这些代码
Useless three(getObject());
getObject()函数创建temp对象,temp对象管理着10000个字符。Useless的复制构造函数将创建这10000个字符的副本,然后删除getObject()函数返回的临时对象。这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将临时对象对数据的所有权直接转让给three对象,不是更好么?也就是说,不将临时对象管理的10000个字符复制到新的地方,再删除临时对象管理的字符串。而是将字符留在原来的地方,并将three对象与之相关联。这类似于在计算机中移动文件的情形,实际的文件还是留在原来的地方,而只修改记录。这种方法被称为移动语义。有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。
三,使用移动构造函数实现移动语义
使用前面介绍的Useless类,这个类里面的很多函数都很常规,在这里只写出移动复制构造函数的定义。
class Useless{
private:
string name;
char *p;
public:
Useless();
Useless(string name);
Useless(const Useless &f);
Useless(Useless &&f);
Useless operator+(const Useless &f) const;
void showData();
~Useless();
};
移动复制构造函数
Useless::Useless(Useless &&f):name(f.name){
p = f.p;
f.p = NULL;
showData();
}
应用实例
int main(){
Useless one("one");
Useless two("two");
Useless three(one + two);
return 0;
}
输出结果
one Object Data Address : 0xcc0d78
two Object Data Address : 0xcc0da8
one+two Object Data Address : 0xcc0e08
one+two Object Data Address : 0xcc0e08
one+two Object delete. 0x000000
one+two Object delete. 0xcc0e08
two Object delete. 0xcc0da8
one Object delete. 0xcc0d78
Process returned 0 (0x0) execution time : 0.012 s
Press any key to continue.
程序分析
在方法operator+中创建的对象的数据地址与对象three存储的数据地址相同(都是 0xcc0e08),其中对象three是由移动复制构造函数创建的。另外,注意到创建对象three之后,为临时对象调用了析构函数。之所以知道这是临时对象,是因为其数据地址是: 0x000000。使用移动复制构造函数直接交换了临时对象与three对象之间的数据的所有权。
四,移动构造函数解析
1,移动构造函数
Useless::Useless(Useless &&f):name(f.name){
p = f.p;
f.p = NULL;
showData();
}
上面的代码使p直接指向现有的数据,以获取这些数据的所有权。此时,p与f.p指向相同的数据,调用析构函数时,这将带来麻烦,因为程序不能为同一个地址调用delete []两次。为避免这个问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete []没有问题。
注意:
由于修改了f对象,这要求不能在参数声明中使用const。
2,移动构造函数解析
要让移动语义发生,需要下面两个步骤。
(1),右值引用让编译器知道何时可使用移动语义
Useless one("one");
Useless two("two");
Useless three(one + two);
对象one与two是左值,与左值引用匹配,而表达式one + two是右值,与右值引用匹配。因此,右值引用让编译器使用移动复制构造函数初始化对象three。
(2),定义移动复制构造函数。
五,移动构造函数与移动赋值运算符
1,C++11在原有4个特殊成员函数的基础上,新增了两个:移动构造函数与移动赋值运算符。默认的移动构造函数与移动赋值运算符与复制版本类似,执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数与移动赋值运算符,这将调用他们。否则将调用复制构造函数和赋值运算符。
2,如果你提供了析构函数、复制构造函数或赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符。如果你提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和赋值运算符。