C++11 右值引用

发布时间:2023年12月18日

C++11是继98/03版本之后的大改版,其中增加了许多新特性,得到广泛的应用。这篇文章就介绍其中的右值引用。希望能够解释明白以下三个问题:
??1.什么是右值引用?
??2.右值引用有什么好处?
??3.右值引用的使用说明

1.什么是右值引用

首先,介绍一下左右值:

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
// 以下的p、b、c、*p都是左值
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;
}

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址。右值引用就是对右值的引用,给右值取别名。

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 C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可 以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地 址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇, 这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{
 double x = 1.1, y = 2.2;
 int&& rr1 = 10;
 const double&& rr2 = x + y;
 rr1 = 20;
 rr2 = 5.5;  // 报错
 return 0;
}

2.左值引用与右值引用比较

左值引用总结:

1. 左值引用只能引用左值,不能引用右值。

2. 但是const左值引用既可引用左值,也可引用右值。?

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

?右值引用总结:

1. 右值引用只能右值,不能引用左值。

2. 但是右值引用可以move以后的左值。

int main()
{
 // 右值引用只能右值,不能引用左值。
 int&& r1 = 10;
 
 // error C2440: “初始化”: 无法从“int”转换为“int &&”
 // message : 无法将左值绑定到右值引用
 int a = 10;
 int&& r2 = a;
 // 右值引用可以引用move以后的左值
 int&& r3 = std::move(a);
 return 0;
}

3.右值引用的好处?

?????????先想想引用的目的,传递参数有两种方式:值传递和引用传递。二者相比引用传递的优势就是通过传递地址,来减少一次拷贝。在常规写程序的时候,使用的都是左值引用。左值引用有两个使用场景:函数传参、函数返回值。
1)函数传参:int f(int &a);
2)函数返回值:int& f();
以上两种情况使用的都是引用传递相比于值传递减少了拷贝次数。但有一种情况会出问题:就是返回值是一个临时对象。如下代码:

A& f() {
	A a;
	return a;
}

当返回对象a的地址时,其实a作为在栈上的临时对象,作用域已经到了,被析构。这样如果外界再对这个地址进行访问时,就会出现问题。这也左值引用的一个弊端,而右值引用的出现就是为了解决这个问题。那右值引用是怎么解决返回的临时变量析构? 当返回值为右值引用时,会把返回的临时变量中的内存居为己用,仍保持了有效性,也避免了拷贝。

?4.右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引 用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

namespace bit
{
 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(char* str)" << endl;
     _str = new char[_capacity + 1];
     strcpy(_str, str);
   }
 // s1.swap(s2)
   void swap(string& s)
   {
     ::swap(_str, s._str);
     ::swap(_size, s._size);
     ::swap(_capacity, s._capacity);
   }
 // 拷贝构造
   string(const string& s)
   :_str(nullptr)
   {
     cout << "string(const string& s) -- 深拷贝" << endl;
     string tmp(s._str);
     swap(tmp);
   }
 // 赋值重载
   string& operator=(const string& s)
   {
      cout << "string& operator=(string s) -- 深拷贝" << endl;
     string tmp(s);
     swap(tmp);
     return *this;
   }
 // 移动构造
   string(string&& s)
   :_str(nullptr)
   ,_size(0)
   ,_capacity(0)
   {
     cout << "string(string&& s) -- 移动语义" << endl;
     swap(s);
   }
 // 移动赋值
   string& operator=(string&& s)
   {
     cout << "string& operator=(string&& s) -- 移动语义" << endl;
     swap(s);
     return *this;
   }
   ~string()
   {
     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; // 不包含最后做标识的\0
 };
}

?左值引用的使用场景: 做参数和做返回值都可以提高效率。

void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
 bit::string s1("hello world");
 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
 func1(s1);
 func2(s1);
 // string operator+=(char ch) 传值返回存在深拷贝
 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
 s1 += '!';
 return 0;
}

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

?

?

namespace bit
{
 bit::string to_string(int value)
 {
   bool flag = true;
   if (value < 0)
   {
     flag = false;
     value = 0 - value;
   }
   bit::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()
{
// 在bit::string to_string(int value)函数中可以看到,这里
// 只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷
贝构造)。
 bit::string ret1 = bit::to_string(1234);
 bit::string ret2 = bit::to_string(-1234);
 return 0;
}

?

右值引用和移动语义解决上述问题:
bit::string 中增加移动构造, 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
// 移动构造
string(string&& s)
 :_str(nullptr)
 ,_size(0)
 ,_capacity(0)
{
 cout << "string(string&& s) -- 移动语义" << endl;
 swap(s);
}
int main()
{
 bit::string ret2 = bit::to_string(-1234);
 return 0;
}
再运行上面 bit::to_string 的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用
了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

?

不仅仅有移动构造,还有移动赋值:
bit::string 类中增加移动赋值函数,再去调用 bit::to_string(1234) ,不过这次是将
bit::to_string(1234) 返回的右值对象赋值给 ret1 对象,这时调用的是移动构造。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
 bit::string ret1;
 ret1 = bit::to_string(1234);
 return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。 bit::to_string 函数中会先用 str 生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把 str 识别成了右值,调用了移动构造。然后在把这个临时
对象做为 bit::to_string 函数调用的返回值赋值给 ret1 ,这里调用的移动赋值。
STL 中的容器都是增加了移动构造和移动赋值:
http://www.cplusplus.com/reference/string/string/string/
http://www.cplusplus.com/reference/vector/vector/vector/
void push_back (value_type&& val);
int main()
{
 list<bit::string> lt;
 bit::string s1("1111");
// 这里调用的是拷贝构造
 lt.push_back(s1);
// 下面调用都是移动构造
 lt.push_back("2222");
 lt.push_back(std::move(s1));
 return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义

5.完美转发

模板中的 && 万能引用

?

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); ? ? ?// const 左值
 PerfectForward(std::move(b)); // const 右值
 return 0;
}
std::forward 完美转发在传参的过程中保留对象原生类型属性
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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
 Fun(std::forward<T>(t));
}
int main()
{
 PerfectForward(10); ? ? ? ? ? // 右值
 int a;
 PerfectForward(a); ? ? ? ? ? ?// 左值
 PerfectForward(std::move(a)); // 右值
 const int b = 8;
 PerfectForward(b); ? ? ?// const 左值
 PerfectForward(std::move(b)); // const 右值
 return 0;
}
完美转发实际中的使用场景:
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<bit::string> lt;
 lt.PushBack("1111");
 lt.PushFront("2222");
 return 0;
}

文章来源:https://blog.csdn.net/ayf9999/article/details/134993161
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。