关于右值和移动构造
Posted 大黑耗
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于右值和移动构造相关的知识,希望对你有一定的参考价值。
右值变量只有内容,没有承载这个内容的实体,他表示一个数据信息,你不能像修改左值那样去修改右值变量,不能去取右值变量的地址(但是右值实际上是不是也像左值变量那样也存储在栈地址中我还不清楚)
右值引用是右值变量的别名,左值引用是左值变量的别名。可以把左值想象为容器(不是stl那个容器),而右值是容器里的内容。
std::move操作是获取一个基本内置类型的右值引用,对一个左值使用move操作后会破坏这个左值!所以要注意move之后这个左值就不能再使用它原来的值了(cpp primer P471)!(但是可以给它赋新值)
移动构造函数依赖move操作。
对左值变量而言,它是存在实体的,对一个基本内置类型左值变量A使用move函数(或者调用他的移动运算符/移动构造函数)来初始化另一个同类变量B,相当与把A的内容剪切到B上,
注意B并不是接管了A的内存地址(从下面的结果看出2者并不在同一地址),而是“窃取”了A的内容,省去了复制构造的操作,比复制构造有更高的效率(以破坏源对象为代价)。
对于我们自定义的类型,在使用一个对象A去构造另一个对象B,而又不再需要原对象A的场景里(因为移动构造会破坏掉源对象),我们可以自定义这个类的移动构造函数,移动操作并没有规定必须是把一个地址的内容移动到另一个地址,
我们自定义的移动构造可以是接管源对象的地址来达到“移动”的目的(或者对类内每个内置类型成员都调用std::move操作)
#include <iostream> using namespace std; int main() string str1("hello"); cout<<"addr str1: "<<&str1<<endl; string str2 = move(str1); cout<<"str2: "<<str2<<" addr str2: "<<&str2<<endl; cout<<"addr str1 after: "<<&str1<<endl; cout<<"str1: "<<str1<<endl; return 0; /* result: addr str1: 0x7fffffffda10 str2: hello addr str2: 0x7fffffffda30 addr str1 after: 0x7fffffffda10 str1: */
❥关于C++之右值引用&移动语义┇移动构造&移动复制
右值引用(C++11)
传统的左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。左值引用使得标识符关联到左值。
右值引用
,这是使用&&
表示的。
右值引用只能关联右值,即可出现在赋值表达式右边。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x+y等表达式及返回值的函数(条件是该函数返回的不是引用):
int x = 10;
int y = 23;
int z = 45;
int&& r = z;// Error:Rvalue reference to type 'int' cannot bind to lvalue of type 'int'
int&& r1 = 13;// OK
int&& r2 = x + y;// OK
double&& r3 = sqrt(2.0);// OK
r2关联到的是当时计算x+y得到的结果。也就是说,r2关联到的是33,即使以后修改了x或y,也不会影响到r2。
将右值关联到右值引用会导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
左值右值?
所谓左值即有内存地址的表达式(包括变量),任何类型的变量如int x就是左值。
所谓右值就是没有内存地址处于缓存的值,即编译时未分配内存或使用寄存器存放的值,例如赋值语句x=3中使用的常量3。
通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法:
1) 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
int a = 5; 5 = a; //错误,5 不能为左值
其中,变量 a 就是一个左值,而字面量 5 就是一个右值。值得一提的是,C++ 中的左值也可以当做右值使用,例如:
int b = 10; // b 是一个左值 a = b; // a、b 都是左值,只不过将 b 可以当做右值使用
2) 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
以上面定义的变量 a、b 为例,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量 5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值。
移动语义
引入右值引用的主要目的之一是实现移动语义。
先来看看C++11之前的复制过程。假设有如下代码:
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
...
vector<string> vstr_copy1(vstr);// make vstr_copy1 a copy of vstr
vector和string类都使用动态内存分配,因此它们必须定义使用某种new版本的复制构造函数。为初始化对象vstr_copy1,复制构造函数vector<string>将使用new给20000个string对象分配内存,而每个string对象又将调用string的复制构造函数,该构造函数使用new为1000个字符分配内存。接下来,全部20000000个字符都将从vstr控制的内存中复制到vstr_copy1控制的内存中。这里的工作量很大,但只要妥当就行。
以上确实妥当吗?有时答案是否定的。
例如,假设有一个函数,它返回一个vector<string>对象,然后使用它:
vector<string> allcaps(const vector<string>& vs)
vector<string> temp;
// code that stores an all-uppercase version of vs in temp
return temp;
——————————————————————————————————————————————————
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
vector<string> vstr_copy1(vstr); // #1
vector<string> vstr_copy2(allcaps(vstr)); // #2
allcaps()创建了对象temp,该对象管理着20000000个字符;vector和string的复制构造函数创建这20000000个字符的副本,然后程序删除allcaps()返回的临时对象。这里的要点是,做了大量的无用功。
考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?也就是说,不将20000000个字符复制到新地方,再删除原来的字符,而将字符留在原来的地方,并将vstr_copy2与之相关联。
这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(move semantics)。
有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。
要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。
可以定义两个构造函数。其中一个是常规复制构造函数,它使用const左值引用作为参数,如语句#1中的vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句#2中allcaps(vstr)的返回值。
复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。
先看一个移动示例:
#include <iostream>
using namespace std;
class Useless
private:
int n;// number of elements
char* pc;// pointer to data
static int ct;// number of objects
void ShowObject() const;
public:
Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless& f);// regular copy constructor
Useless(Useless&& f);// move constructor
~Useless();
Useless operator+(const Useless& f) const;
// need operator=() in copy and move versions
void ShowData() const;
;
int Useless::ct = 0;
Useless::Useless() // 【默认构造函数】
++ct;
n = 0;
pc = nullptr;
cout << "default constructor called; number of objects: " << ct << endl;
ShowObject();
Useless::Useless(int k) : n(k) // 【(int)构造函数】
++ct;
cout << "(int) constructor called; number of objects: " << ct << endl;
pc = new char[n];
ShowObject();
Useless::Useless(int k, char ch) : n(k) // 【(int,char)构造函数】
++ct;
cout << "(int, char) constructor called; number of objects: " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = ch;
ShowObject();
Useless::Useless(const Useless& f) : n(f.n) // 【复制构造函数】
++ct;
cout << "copy constructor called; number of objects: " << ct << endl;
pc = new char[n];// 深拷贝
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
ShowObject();
Useless::Useless(Useless&& f) : n(f.n) // 【★移动构造函数★】
++ct;
cout << "move constructor called; number of objects: " << ct << endl;
pc = f.pc;// steal address
f.pc = nullptr;// give old object nothing in return
f.n = 0;
ShowObject();
Useless::~Useless() // 【析构函数】
cout << "destructor called; objects left: " << --ct << endl;
cout << "deleted object:\\n";
ShowObject();
delete[] pc;
Useless Useless::operator+(const Useless& f) const // 【重载加法运算符】
cout << "Entering operator+()\\n";
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
temp.pc[i] = pc[i];
for (int i = n; i < temp.n; i++)
temp.pc[i] = f.pc[i - n];
cout << "temp object:\\n";
cout << "Leaving operator+()\\n";
return temp;
void Useless::ShowObject() const // 【打印成员变量:n和pc地址】
cout << "Number of elements: " << n;
cout << ", Data address: " << (void*)pc << endl;
void Useless::ShowData() const // 【打印成员变量pc指针所代表的字符串】
if (n == 0)
cout << "(object empty)";
else
for (int i = 0; i < n; i++)
cout << pc[i];
cout << endl;
int main()
Useless one(10, 'x');
Useless two = one;// calls copy constructor
Useless three(20, 'o');
Useless four(one + three);// calls operator+()、move constructor AND destructor
cout << "object one: ";
one.ShowData();
cout << "object two: ";
two.ShowData();
cout << "object three: ";
three.ShowData();
cout << "object four: ";
four.ShowData();
return 0;
(int, char) constructor called; number of objects: 1
Number of elements: 10, Data address: 007CF2E0
copy constructor called; number of objects: 2
Number of elements: 10, Data address: 007CF468
(int, char) constructor called; number of objects: 3
Number of elements: 20, Data address: 007CD960
Entering operator+()
(int) constructor called; number of objects: 4
Number of elements: 30, Data address: 007CBEE8
temp object:
Leaving operator+()
move constructor called; number of objects: 5
Number of elements: 30, Data address: 007CBEE8
destructor called; objects left: 4
deleted object:
Number of elements: 0, Data address: 00000000
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of elements: 30, Data address: 007CBEE8
destructor called; objects left: 2
deleted object:
Number of elements: 20, Data address: 007CD960
destructor called; objects left: 1
deleted object:
Number of elements: 10, Data address: 007CF468
destructor called; objects left: 0
deleted object:
Number of elements: 10, Data address: 007CF2E0
移动构造函数
和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,pc指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了f.pc、f.n,有效避免了“同一块对空间被释放多次”情况的发生。
它让pc指向现有的数据,以获取这些数据的所有权。此时,pc和f.pc指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用delete [ ]两次。为避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete [ ]没有问题。这种夺取所有权的方式常被称为窃取(pilfering)。上述代码还将原始对象的元素数设置为零,这并非必不可少的,但让这个示例的输出更一致。注意,由于修改了f对象,这要求不能在参数声明中使用const。
对于Useless four(one + three); 调用了移动构造函数。
表达式one + three调用Useless::operator+(),而右值引用 f 将关联到该函数返回的临时对象。
注意到对象two是对象one的副本:它们显示的数据输出相同,但显示的数据地址不同(007CF2E0和007CF468)。另一方面,在方法Useless::operator+()中创建的对象的数据地址与对象four存储的数据地址相同(都是007CBEE8),其中对象four是由移动复制构造函数创建的。另外,注意到创建对象four后,为临时对象调用了析构函数。之所以知道这是临时对象,是因为其元素数和数据地址都是0。
当没有移动构造函数时,调用复制构造函数;移动构造和复制构造函数都有,生成临时对象时会调用移动构造。
移动赋值运算符
适用于构造函数的移动语义考虑也适用于赋值运算符。
下面演示了给Useless类编写复制赋值运算符和移动赋值运算符:
Useless& Useless::operator=(const Useless& f) // copy assignment
if (this == &f)
return *this;
delete[] pc;
n = f.n;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
return *this;
Useless& Useless::operator=(Useless&& f) // move assignment
if (this == &f)
return *this;
delete[] pc;
n = f.n;
pc = f.pc;
f.n = 0;
f.pc = nullptr;
return *this;
上述移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标。
不能让多个指针指向相同的数据,这很重要,因此上述代码将源对象中的指针设置为空指针。
与移动构造函数一样,移动赋值运算符的参数也不能是const引用,因为这个方法修改了源对象。
一般而言,编译器完全可以进行优化,只要结果与未优化时相同。即使你省略该程序中的移动构造函数。
机智的编译器可能自动消除额外的复制工作,但通过使用右值引用,程序员可指出何时该使用移动语义。
深度理解:
为什么左值引用不需要实现移动构造呢?知道左值和右值的区别,这个问题就容易回答和解决了。所谓左值即有内存地址的表达式(包括变量),任何类型的变量如int x就是左值。所谓右值就是没有内存地址处于缓存的值,即编译时未分配内存或使用寄存器存放的值,例如赋值语句x=3中使用的常量3。注意这样的常量在汇编语言中又叫立即数,例如“MOVE EAX, 3”中的立即数3就是无址的右值,立即数用过即丢弃;而作为常量对象,就意味要进行析构。
对于类A的移动构造函数A(A&& a),形参a是一个右值,即它要求实参是一个常量对象。在定义“A x(A(3));”时,要构造变量对象x,当然先要构造常量对象A(3),构造完x要析构常量对象A(3)。若构造函数分配内存,析构函数释放内存,则合计有两次申请和一次释放。若将常量对象A(3)申请的内存移交给变量对象x,不用x申请了,并且让常量对象A(3)以后不释放内存,岂不是提高了程序的效率吗?反正常量对象A(3)用过就要丢弃的。
但对于构造函数 A(const A& v)就不一样了,形参v是左值要求实参是有址的,即实参通常是一个变量对象。在定义“A y(ā);”时,你不能立即析构 ā 释放其分配的内存,因为变量对象 ā 的生命期还没到死亡时刻,不像常量对象A(3)需要马上死亡,所以不能立即析构 ā,也不能将A(const A& v)实现为移动构造,而只能将其实现为深拷贝构造。
以上是关于关于右值和移动构造的主要内容,如果未能解决你的问题,请参考以下文章
c++复习笔记——右值引用(概念,使用场景),移动拷贝构造函数,赋值拷贝构造函数。
c++复习笔记——右值引用(概念,使用场景),移动拷贝构造函数,赋值拷贝构造函数。