1. 左值引用和右值引用
在C++11之前出现的引用都是左值引用,右值引用是C++11新增的。但是无论左值引用还是右值引用,都是给对象取别名。
1.1 左值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
//左值
int* p = new int(0);
int b = 1;
const int c = 2;
//左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
1.2 右值
int main()
{
double x = 1.1, y = 2.2;
//右值
10;
x + y;
fmin(x, y);//函数返回值
//右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
return 0;
}
2. 移动语义
为什么要使用引用?因为引用可以减少拷贝。左值引用就解决了传参拷贝问题和部分返回对象问题(这里的对象是出了函数,生命周期还没结束的,可以返回该对象的引用减少拷贝),但是如果返回的对象生命周期在函数作用域内(将亡值),那出了函数,只能返回该对象的拷贝,编译器一般会对这个过程优化:拷贝这个对象出一个临时对象,再返回这个临时对象的拷贝-------> 直接返回这个对象的拷贝。浅拷贝还好,深拷贝就很浪费资源了。
所以为了解决上面的问题,就引出了右值引用和移动语义。首先要明确,右值引用单独使用是没有多大意义的,右值引用要和移动语义一起使用。
2.1 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
My::string to_string(int value)
{
My::string res;
//value -> res
return res;
}
int main() {
My::string res1 = to_string(1234);
return 0;
}
//string(string&& s) -- 移动语义
在to_string函数中,编译器会隐式的move转换,把res转化为右值即将亡值(这个隐式编译器不一定会实现,得看版本)。如果拷贝构造和移动构造都实现了,右值会优先使用移动构造,在这个过程中,先对res使用移动构造出一个临时对象,再返回这个对象的移动构造对象,编译器会优化为直接返回用res移动构造的对象,即从两次的移动构造减少为一次。
2.2 移动赋值
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);//自定义的swap
return *this;
}
int main() {
My::string ret1;
ret1 = to_string(1234);
return 0;
}
//string(string&& s) -- 移动语义
//string& operator=(string&& s) -- 移动语义
string (string&& str) noexcept;
vector (vector&& x);vector (vector&& x, const allocator_type& alloc);
3. 完美转发
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10);
int a;
PerfectForward(a);
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b);
PerfectForward(std::move(b)); // const 右值
return 0;
}
//左值引用
//左值引用
//左值引用
//const 左值引用
//const 左值引用
上面的执行结果可能让你感到奇怪,为什么无论是左值还是右值,结果都是左值引用呢?因为右值引用后的对象的属性是左值,即PerfectForward接受的参数是左值或右值,但是Fun的参数t只会是左值。所以为了在传递过程中保持左值或右值的属性不变就需要完美转发了。
//std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
//右值引用
//左值引用
//右值引用
//const 左值引用
//const 右值引用
实际应用中,forward是很重要的,以List 为例:
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode; newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
4. 右值引用引用左值及其一些更深入的使用场景分析
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
void swap(string& ss)
{
::swap(_str, ss._str);
::swap(_size, ss._size);
::swap(_capacity, ss._capacity);
}
//std::list::push_back
void push_back (const value_type& val);
void push_back (value_type&& val);
//std::vector::push_back
void push_back (const value_type& val);
void push_back (value_type&& val);