【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


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

相关文章
|
6天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
21 2
|
12天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
40 5
|
19天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
49 4
|
20天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
46 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
54 1