【C++】右值引用和移动语义 | 新的类功能 | 可变参数模板(上)

简介: 【C++】右值引用和移动语义 | 新的类功能 | 可变参数模板(上)

👉左值引用和右值引用👈


左值引用和右值引用


传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。


什么是左值?什么是左值引用?


左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时 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);
  // 下面的语句都会编译报错,左操作数必须为左值
  //10 = 1;
  //x + y = 1;
  //fmin(x, y) = 1;
  return 0;
}


左值可以引用右值吗?右值可以引用左值吗?


// x既能接受左值,又能接受右值
template <class T>
void Func(const T& x)
{
  //...
}
int main()
{
  // 左值引用可以引用右值吗?const的左值引用可以
  double x = 1.1, y = 2.2;
  //double& rr1 = x + y;  // 编译报错
  const double& rr2 = x + y; // 可以
  // 右值引用可以引用左值吗?不可以,可以引用move以后的左值
  int a = 10;
  //int&& rr3 = a; // 编译报错
  int&& rr5 = move(a);
  return 0;
}


左值引用与右值引用总结:左值引用只能引用左值,不能引用右值。但是 const 左值引用既可引用左值,也可引用右值。右值引用只能引用右值,不能引用左值,但是右值引用可以 move 以后的左值。


需要注意的是:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。也就是说,不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1。如果不想 rr1 被修改,可以用 const int&& rr1 去引用。注:rr1 和 rr2 都是左值。


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

8477ecf169524728a1edac36057e7be8.png


右值引用使用场景和意义


左值引用解决的问题:

  1. 做参数:a. 减少拷贝,提高效率。b. 做输出型参数
  2. 做返回值:a. 减少拷贝,提高效率。b. 引用返回,可以修改返回对象(比如:operator[]



左值引用既可以引用左值和又可以引用右值,那为什么C++11 还要提出右值引用呢?其实左值引用无法解决一些场景的问题,所以就提出了右值引用。

98f7f47c84644bd48d48c754d56301be.png


C++11 的右值引用的一个重要功能就是要解决上面的问题,但右值引用并不是直接作为返回值起作用的。

4e9a3a60cc39408a891b8874e816b4cd.png

18447b71c7ba449b8e75fcf5140b7035.png


注:只有在一个表达式里,编译器才能够优化,上图的场景无法优化。对象已经存在,只能用to_string生成的临时对象调用赋值运算符重载赋值给ret


那么在string类里添加移动构造和移动赋值就能够解决上面的问题了。


// 移动构造
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;
}

d2ec46b38cf2491f904ea0d6b2bdaa5b.png


792bb98134ec40c8a8b591d3212487fe.png


9b6ae7d8a5644a1387e0ac06b0cdb4c2.png

4d045fa67b524869acb34d75258c9125.png


添加移动构造和移动赋值后,就不会存在深拷贝了。在bit::string中增加移动构造和移动赋值,移动构造和移动赋值本质是将参数右值的资源转移到指定的对象中,那就不需要深拷贝了。


注:右值有两类,第一类是纯右值,即内置类型右值;第二类是将亡值,即自定义类型右值。右值将亡值的资源可以转移到指定的对象,而左值不能。移动构造和移动赋值是延长了资源的生命周期。


4677c7878e19454a9ddd4d57ac09c559.png

完整代码


namespace Joy
{
  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);
      // 传统写法
      _str = new char[s._capacity + 1];
      strcpy(_str, s._str);
      _size = s._size;
      _capacity = s._capacity;
    }
    // 赋值重载
    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
  };
  string to_string(int value)
  {
    bool flag = true;
    if (value < 0)
    {
      flag = false;
      value = 0 - value;
    }
    string str;
    while (value > 0)
    {
      int x = value % 10;
      value /= 10;
      str += ('0' + x);
    }
    if (flag == false)
    {
      str += '-';
    }
    reverse(str.begin(), str.end());  
    return str;
  }
}


STL 中的容器都是增加了移动构造和移动赋值的。


c50ce9e0b8464b719f8e2fc5b968cc72.png


STL 容器的插入接口函数也增加了右值引用版本。


e70140dff2cc49c8a3d1b2c58c869772.png

5587db13be48495d91bb51e5217df764.png

e7a899e77f9d4529912001d63b9bb9df.png

完美转发


模板中的 && 万能引用


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;
}


854ee039ed624e14b6d8368a2b1a980c.png


可以看到,上面的引用通通被折叠成左值引用。其实这可以用上面的一个知识点来解释,当右值引用右值时,那么这个引用的属性也是左值,其有自己的地址。


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; }
// 万能引用:既能引用左值,也能引用右值
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;
}


2aa5b337cff741b2a52cdcab5e7270ec.png


注:如果想要一直保持对象的元素类型,就要一直完美转发。注:只有模板参数采用万能引用,确定的类型没有万能引用。

相关文章
|
1天前
|
C++
【C++基础】类class
【C++基础】类class
9 1
|
1天前
|
安全 程序员 编译器
C++程序中的基类与派生类转换
C++程序中的基类与派生类转换
8 1
|
1天前
|
C++
C++程序中的类成员函数
C++程序中的类成员函数
7 1
|
6天前
|
设计模式 安全 算法
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
【C++入门到精通】特殊类的设计 | 单例模式 [ C++入门 ]
19 0
|
1天前
|
C++
C++程序中的类封装性与信息隐蔽
C++程序中的类封装性与信息隐蔽
8 1
|
1天前
|
C++
C++程序中的类声明与对象定义
C++程序中的类声明与对象定义
9 1
|
1天前
|
数据安全/隐私保护 C++
C++程序中的派生类
C++程序中的派生类
7 1
|
1天前
|
C++
C++程序中的派生类成员访问属性
C++程序中的派生类成员访问属性
8 1
|
1天前
|
编译器 C++
C++程序中的派生类析构函数
C++程序中的派生类析构函数
9 2
|
4天前
|
测试技术 C++
C++|运算符重载(3)|日期类的计算
C++|运算符重载(3)|日期类的计算