cpp►C++11右值引用移动语义移动构造函数移动赋值运算符
Posted itzyjr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了cpp►C++11右值引用移动语义移动构造函数移动赋值运算符相关的知识,希望对你有一定的参考价值。
目录
右值引用
传统的C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。修饰符const的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址。
C++11新增了右值引用
,这是使用&&
表示的。右值引用可以关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。
右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x+y等表达式及返回值的函数(条件是该函数返回的不是引用):
int x = 10;
int y = 23;
int&& r1 = 13;
int&& r2 = x + y;
double&& r3 = std::sqrt(2.0);
r2关联到的是当时计算x+y得到的结果。也就是说,r2关联到的是33,即使以后修改了x或y,也不会影响到r2。
将右值关联到右值引用会导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于13,但可将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
下面是一个简短示例,演示了上述有关右值引用的要点:
#include <iostream>
inline double f(double tf) return (tf - 32.0) / 1.8;
int main()
double tc = 21.5;
double&& rd1 = 7.07;
double&& rd2 = 1.8 * tc + 32.0;
double&& rd3 = f(rd2);
cout << " tc value and address: " << tc << ", " << &tc << endl;
cout << "rd1 value and address: " << rd1 << ", " << &rd1 << endl;
cout << "rd2 value and address: " << rd2 << ", " << &rd2 << endl;
cout << "rd3 value and address: " << rd3 << ", " << &rd3 << endl;
return 0;
tc value and address: 21.5, 00D3F9E4
rd1 value and address: 7.07, 00D3F9C8
rd2 value and address: 70.7, 00D3F9AC
rd3 value and address: 21.5, 00D3F990
引入右值引用的主要目的之一是实现移动语义。
移动语义
先来看看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;
// interface
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;
;
// implementation
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();
上面程序,每调用任何一种构造函数,即构造对象时,ct++;每调用析构函数,即销毁对象时,ct–。
在VS2019上运行结果如下:
(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
程序解读:
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();
移动构造函数
和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,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引用,因为这个方法修改了源对象。
一般而言,编译器完全可以进行优化,只要结果与未优化时相同。即使您省略该程序中的移动构造函数。
机智的编译器可能自动消除额外的复制工作,但通过使用右值引用,程序员可指出何时该使用移动语义。
以上是关于cpp►C++11右值引用移动语义移动构造函数移动赋值运算符的主要内容,如果未能解决你的问题,请参考以下文章
C++11之右值引用:移动语义和完美转发(带你了解移动构造函数纯右值将亡值右值引用std::moveforward等新概念)