C++11中新增了右值引用语法特性,所以之前所学的引用均为左值引用。无论是左值引用还是右值引用,都是给对象取别名。
左值和左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),**我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。**定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
cpp
int main()
{
int* p = new int(0);
int b = 1;
const int c = 2;
//以上的p,b,c,*p都是左值
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
//以上几个是对上面左值的左值引用
return 0;
}
右值和右值引用
右值也是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值(不能是左值引用返回)等等。**右值可以出现在赋值符号的右边,不能出现在赋值符号的左边,右值不能取地址。**右值引用就是给左值的引用,给右值取别名。
cpp
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);
//以上几个都是对右值的右值引用。
//以下编译会报错:error:"=":左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。不过这个特性不重要。
左值引用与右值引用比较
左值引用相关:
1.左值引用只能引用左值,不能引用右值。
2.但const左值引用既可引用左值,也可引用右值。

右值引用相关:
1.右值引用只能引用右值,不能引用左值。
2.但右值可以引用move以后的左值。

右值引用的使用场景和意义
cpp
namespace Q
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(const char* str = "")" << endl;
_str = new char[_capacity+1];
strcpy(_str, str);
}
void swap(string& s)
{
std::swap(s._str,_str);
std::swap(s._size,_size);
std::swap(s._capacity,_capacity);
}
string(const string& s)
: _size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
cout << "string(const string& s)--深拷贝" << endl;
}
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s)--移动拷贝" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s)--深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)--移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string()" << endl;
delete[] _str;
_str = nullptr;
}
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)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string to_string(int value);
}
//Q::string to_string(int value)
Q::string Q::to_string(int value)
{
Q::string str;
//...
return str;
}
左值引用的使用场景:
做参数和返回值都可以提高效率
cpp
void func1(Q::string s)
{ }
void func2(const Q::string& s)
{ }
int main()
{
Q::string s1("hello world");
func1(s1);
func2(s1);
//func1和func2对比可看到左值引用做参数减少了拷贝,提高效率。
//string operator+=(char ch)
//传值返回存在深拷贝
//string& operator+=(char ch)
//传左值引用 没有拷贝 提高了效率
s1+='&';
return 0;
}
左值引用短板:
当函数返回对象是一个局部变量时不能使用左值引用返回,只能传值返回。传值返回会导致至少一次拷贝构造(旧一点的编译器可能是两次拷贝构造)。

本来应该是两次拷贝构造,但是新一点的编译器一般会优化,优化后变成一次拷贝构造。
to_string的返回值是一个右值,用这个右值构造ret1,如果没有移动构造,调用就会匹配调用拷贝构造,因为const左值引用是可以引用右值的,这里就是一个深拷贝。
右值引用和移动语义解决上述问题:
在Q::string中增加移动构造,移动构造本质是将右值的资源窃取过来占为己有,不用做深拷贝了,所以叫做移动构造,就是利用资源转移来构造自己。
所以场景1:自定义类型中深拷贝的类必须传值返回的场景。
cpp
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s)--移动拷贝" << endl;
swap(s);
}
int main()
{
Q::string ret1 = Q::to_string(-366);
return 0;
}
再运行上面的代码,会发现调用了移动构造。
cpp
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)--移动赋值" << endl;
swap(s);
return *this;
}
int main()
{
Q::string ret1;
ret1 = Q::to_string(-366);
return 0;
}
// 运行结果:
// string(string&& s) -- 移动拷贝
// string& operator=(string&& s) -- 移动赋值
运行后调用了一次移动构造和一次移动赋值。如果是用一个已经存在的对象接收,编译器没办法优化。Q::string函数会先用str构造生成一个临时对象,这里编译器将str识别成右值,调用了移动构造。然后将临时对象作为Q::string函数调用的返回值赋值给ret1,这里调用的是移动赋值。
右值引用引用左值及其它使用场景分析
当需要右值引用去引用一个左值时,可通过move函数将左值转化为右值。
cpp
int main()
{
Q::string s1("hello world");
Q::string s2(s1);
Q::string s3(move(s1));
//这里调用移动构造,但一般不要这样用,因为s1的资源被转移给了s3,s1被置空了。
return 0;
}

场景二:容器的插入接口,如果插入对象是右值,可利用移动构造转移给数据结构中的对象,也减少拷贝提高效率。


完美转发
模板中的&&万能引用
模板中的&&不代表右值引用,而是万能引用,既能接收左值又能接收右值。
但引用模板的唯一左右就是限制了接收的类型,后续使用中都退化成了左值。
我们希望它在传递的过程中保持它的左值或者右值属性,需要用完美转发。
cpp
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(move(a)); // 右值
const int b = 8;
PerfectForward(b);// const 左值
PerfectForward(move(b)); // const 右值
return 0;
}
完美转发实际中的使用场景:
cpp
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
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;
}
private:
Node* _head;
};
int main()
{
List<Q::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
