3、移动语义(移动构造 + 移动赋值)(重点 + 难点)
3.1 左值引用 VS 右值引用
笔者在C/C++(二)中介绍的引用叫做左值引用,是传统C++语法中的引用;而右值引用,是C++11标准中新增的语法概念。
左值引用:对左值的引用,为左值取别名。所谓左值,就是可以出现在赋值符号左边(也可以出现在赋值符号右边,表示要赋值给其他对象的源对象)的值,一般都是变量 / 解引用的指针 / 对象标识符,可以获取其地址(左值最重要的核心特点),也可以对其赋值。(PS:const符号修饰后的左值,不能多次赋值,但是仍可以取其地址)
右值引用:对右值的引用,为右值取别名。所谓右值,就是只能出现在赋值符号右边的值, 一般不能被修改,常见的右值有——常量、表达式的返回值、函数的返回值;右值又分为纯右值(往往是内置类型的右值)和将亡值(往往是自定义类型的右值)。纯右值不能取地址。
move函数:可以把左值转右值,右值转左值。(PS:但是不要轻易把自定义类型的左值 move 成右值,可能会被识别成将亡值,将亡值会在移动构造 / 移动赋值中导致资源被转移,导致源对象失去其资源)
注意:
左值引用只能引用左值,但是const左值引用可以引用右值(因为右值不能修改, const左值引用也会让引用不能修改。)
右值引用只能引用右值 / move后的左值。
3.2 左值引用的使用场景
直接上代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;
namespace dfwm
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 构造函数
string(const char* str = "") :_size(strlen(str)), _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造函数
string(const string& s):_str(nullptr)
{
cout << this << " string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值运算符重载
string& operator=(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
void func1(dfwm::string s)
{
}
void func2(const dfwm::string& s)
{
}
int main()
{
dfwm::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
printf("func1:\n");
func1(s1);
printf("func2:\n");
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有深拷贝提高了效率
s1 += '!';
return 0;
}
根据运行结果我们可以发现,传引用并没有调用拷贝构造函数进行深拷贝,提高了效率。
所以左值引用做参数和做返回值都可以有效提高效率。
3.3 左值引用的短板
因为引用是别名,所以如果函数的返回对象是个局部变量,出了函数作用域就销毁了,这时候就不能左值引用返回了,否则会造成未定义行为。
继续上代码:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;
namespace dfwm
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 构造函数
string(const char* str = "") :_size(strlen(str)), _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造函数
string(const string& s) :_str(nullptr)
{
cout << this << " string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值运算符重载
string& operator=(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
dfwm::string& to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
dfwm::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
int main()
{
dfwm::string s1 = dfwm::to_string(1234);
printf("%s", s1.c_str());
return 0;
}
我们为 to_string函数传引用返回,由于 str 是个局部变量,会发现打印出来乱码,这是极其危险的。但是如果传值返回的话,因为是深拷贝,效率会显得很低。
这时候就需要右值引用和移动语义来解决了。
3.4 右值引用和移动语义
3.4.1 移动构造(使用时机:创建新的对象期间)
我们在dfwm::string 里面添加移动构造函数,移动构造函数的构成和拷贝构造函数类似,只是参数由左值引用变成右值引用。
// 移动构造函数
string(string&& s) :_str(nullptr)
{
cout << "string(string&& s) —— 移动构造" << endl;
swap(s);
}
从运行结果可以发现,虽然仍是传值返回,但是因为有移动构造函数,调用了移动构造函数,可以提高效率。
移动构造的原理
在介绍原理之前,我们给右值里面的将亡值下一个明确的定义:
将亡值其实就是出了作用域就会被销毁的对象。
而移动构造,就是把这个将亡值(这也就是为什么移动构造的参数必须是右值引用)的资源直接窃取过来,不用再做深拷贝开辟新空间创建新对象拷贝数据了,提高了效率。
之所以叫移动构造,就是因为相当于把别人的资源移动过来构造自己。
3.4.2 移动赋值(使用时机:两个都已经存在的对象之间赋值)
移动赋值就类似于拷贝赋值,只不过参数同样从左值引用变为右值引用。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) —— 移动赋值" << endl;
swap(s);
return *this;
}
int main()
{
dfwm::string s;
s = dfwm::to_string(1234);
printf("%s", s.c_str());
return 0;
}
运行可以发现,本来的传值赋值,变成了调用移动赋值,提高了效率。
移动赋值的原理
移动赋值的原理类似于移动构造,只不过,由于移动赋值是两个存在的对象之间进行赋值,所以底层原理是把将亡值的资源移动给自己,把自己的废弃资源转移给将亡值,随着将亡值的生命终结,废弃资源也被释放。
3.5 总结
右值引用与移动语义的出现,让传值调用和传值返回的效率都得到了有效提高;在 STL 容器里普遍都添加了移动构造和移动赋值。