C++11 右值引用和移动语义(二)

简介: C++11 右值引用和移动语义

右值引用的使用场景和意义

虽然使用const修饰的左值引用能够同时引用左值和右值 但是左值引用终究是存在一些缺陷的 而C++11提出的右值引用正是用来解决这些缺陷的

为了更好的暴露出左值引用的缺陷 我们写出一个简单string类出来

代码如下

namespace shy
{
  class string
  {
  public:
    typedef char* iterator;
    // begin迭代器返回第一个元素
    iterator begin()
    {
      return _str;
    }
    // end迭代器返回最后一个元素后一个元素的位置
    iterator end()
    {
      return _str + _size;
    }
    // 构造函数
    string(const char* str = "")
    {
      _size = strlen(str); // 初始化设置字符串大小
      _capacity = _size;
      _str = new char[_capacity + 1]; // 这里要多开一个空间来存储/0
      strcpy(_str, str);
    }
    // 交换两个对象的数据 
    void swap(string& s)
    {
      ::swap(_str, s._str);
      ::swap(_size, s._size);
      ::swap(_capacity, s._capacity);
    }
    // 拷贝构造函数
    string (const string& s)
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(const string& s)" << endl;
      string tmp(s._str); // 调用构造函数
      swap(tmp);  // 交换私有成员变量
    }
    // 赋值运算符重载(现代写法)
    string& operator=(const string& s)
    {
      cout << "string& operator=(const string& s)" << endl;
      string tmp(s); //拷贝构造出一个临时变量
      swap(tmp);// 交换这个两个对象
      return *this; // 返回左值
    }
    // 析构函数
    ~string()
    {
      delete[] _str; // 释放str的空间
      _str = nullptr;
      _size = 0;
      _capacity = 0;
    }
    //[]运算符重载
    char& operator[](size_t i)
    {
      assert(i < _size);
      return _str[i]; // 返回左值引用
    }
    // 改变容量 大小不变
    void reserve(size_t n)
    {
      if (n > _capacity)
      {
        char* tmp = new char[n + 1];
        strncpy(tmp, _str, _size + 1); // 这里不使用strcpy的原因是字符串中哟i可能出现/0
        delete[] _str;
        ::swap(_str,tmp);
        _capacity = n; // 容量改变
      }
    }
    // 尾插字符
    void push_back(char ch)
    {
      // 首先判断容量是否足够
      if (_size >= _capacity)
      {
        reserve(_capacity == 0 ? 4 : 2 * _capacity);
      }
      // 尾插到最后 最后加上\0
      _str[_size] = ch;
      _str[_size + 1] = 0;
      _size++; 
    }
    //+=运算符重载
    string& operator+=(char ch)
    {
      push_back(ch);
      return *this;
    }
    // 返回c类型的字符串
    const char* c_str() const //这里加const是修饰this指针 让它的权限变成只读 为了防止后面的只读对象调用这个函数
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
}


左值引用的使用场景

在说明左值引用的缺陷之前 我们先来看它的使用场景

  • 做参数 防止传参时进行拷贝构造
  • 做返回值 防止返回时对返回对象进行拷贝构造
void func1(shy::string s)
{}
void func2(const shy::string& s)
{}
int main()
{
  shy::string s("hello world");
  func1(s); // 值传参
  func2(s); // 左值引用传参
  s += 'X'; // 左值引用返回
  return 0;
}

首先是 func1 它传递的参数是形式参数 他是实际参数的一份临时拷贝

再者是 func2 它传递的参数是s的别名 是左值引用

最后是+=的返回 它返回的也是一份左值引用

我们都知道string的拷贝是深拷贝 深拷贝的代价是很高的 所以说这里的左值引用效果很明显


左值引用的缺陷

左值引用虽然能避免不必要的拷贝操作 但是缺不能完全避免

  • 左值引用做参数 能够完全避免传参时的拷贝操作
  • 左值引用做返回值 不能完全避免函数对象返回时的拷贝操作


如果函数返回对象是一个局部变量 那么该变量出了局部作用域就会被销毁了

这种情况下不能使用左值引用作为返回值 只能传值返回 这就是左值引用的短板

比如说我们实现一个to_string函数 将字符串转化为int类型 此时它的返回值就必须要是值拷贝 如果使用左值引用返回就会返回一个销毁的局部变量


代码表示如下

namespace shy 
{
  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 += (x + '0');
    }
    if (flag == false)
    {
      str += '-';
    }
    std::reverse(str.begin(), str.end());
    return str;
  }
}

我们在调用to_string函数返回的时候会调用拷贝构造函数

58c33f838bba45c8b1d9b03c2b68bc1c.png

C++11提出右值引用就是为了解决左值引用的这个缺陷

但是它的解决方法并不是单纯的将右值引用作为返回值


右值引用和移动语义

右值引用和移动语句解决上述问题的方式就是增加移动构造和移动赋值方法

移动构造


移动构造是一个构造函数 它的参数是右值引用类型的

移动构造的本质就是将传入右值的资源窃取过来 占为己有

这样就避免进行深拷贝 所以叫他移动构造

我们在string类中增加一个移动构造函数 该函数要做的就是通过调用swap函数将传入右值的资源窃取过来

为了能够知道移动构造是否被调用 我们这里增加一条打印语句


代码表示如下

    // 移动构造 
    string(string&& s)
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(string&& s)" << endl;
      swap(s);
    }


移动构造和拷贝构造的区别:

  • 在没有增加移动构造之前 由于拷贝构造使用的是const左值引用来接受参数 因此无论拷贝构造的是左值还是右值 都会调用拷贝构造函数
  • 增加移动构造之后 由于移动构造采用的是右值引用来接受参数 因此如果拷贝构造对象时传入的是右值 那么就会调用移动构造
  • 拷贝构造进行的是深拷贝 而移动构造只需要调用swap函数进行资源转移即可 因此移动构造的代价比拷贝构造的代价小很多


给string类增加移动构造之后 对于返回局部string类对象的函数 返回string类对象的时候会调用移动构造进行资源的转移 不会像原来一样进行深拷贝了

演示效果如下

int main()
{
  shy::string s = shy::to_string(123);
  return 0;
}

9239b1057be549fd93a534af60e2b8e6.png

这里说明一下

  • 对于to_string当中返回局部的string对象是一个左值 一个临时变量 由于它出了局部作用域就会被销毁 被消耗的值我们将它叫做 “将亡值” 匿名对象也可以被称为 “将亡值”
  • 既然 “将亡值” 马上就要被销毁了 那还不如直接把它的资源转移给别人用 因此对待这种 “将亡值” 编译器会将它识别为右值 这样就可以匹配搭配参数为右值的移动构造函数

编译器优化


当一个函数在返回局部对象时 会先用局部对象拷贝出一个临时对象 然后再用这个临时拷贝的对象再来接受我们返回值的对象

效果图如下

61432cbf8c0d4c79adc8360c72b1b05c.png在C++11标准出来之前 对于深拷贝的类会进行两次深拷贝 但是大部分编译器为了提高效率都对这种情况进行了优化 优化成了一次深拷贝 效果图如下

f844dae9d3494c41a10092f85f987ed7.png

但是并不是所有编译器都做这个优化的

在C++11标准出来之后 编译器的这个优化仍然起到了作用

  • 如果不进行优化 这里应该会调用两次移动构造
  • 如果进行了优化 这里就只会进行一次移动构造了
  • 但是实际上有了移动构造之后这里优化的作用就不大了 因为移动构造本质上就是资源转移 很轻松就能做到 资源消耗不大


但是我们如果不是用函数的返回值来构造出一个对象 而是用一个之前已经定义过的对象来接受函数的返回值 这里就无法进行优化了

示例图如下

d0bbbce7effd4e279e3e9cd81c767ba0.png


我们来看看实际代码运行过程中是什么样子的

shy::string s;
  s = shy::to_string(123);

94c580797e20440bbb1cb862b7651b3c.png

当函数返回局部对象的时候 会先用移动构造构造出一个临时对象 然后再调用赋值运算符重载函数将这个临时对象赋值给接收函数返回值的对象 而赋值运算符重载函数本质上是对于拷贝构造的复用 所以说最后还会调用一次拷贝构造函数

  • 编译器并没有对这种情况进行优化 因此在C++11标准出来之前 对于深拷贝的类来说这里就会存在两次深拷贝 因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现
  • 但在深拷贝的类中引入C++11的移动构造后 这里仍然需要再调用一次赋值运算符重载函数进行深拷贝 因此深拷贝的类不仅需要实现移动构造 还需要实现移动赋值


这里需要说明的是 对于返回局部对象的函数 就算只是调用函数而不接收该函数的返回值 也会存在一次拷贝构造或移动构造 因为函数的返回值不管你接不接收都必须要有 而当函数结束后该函数内的局部对象都会被销毁 所以就算不接收函数的返回值也会调用一次拷贝构造或移动构造生成临时对象

移动赋值


移动赋值是对于赋值运算符重载的一个重载函数 该函数的参数是右值引用类型的

移动赋值和移动构造一样 都是将临时对象的资源窃取过来据为己有 这样就避免了深拷贝

在当前的string类中增加一个移动赋值函数 就是调用swap函数将传入右值的资源窃取过来

代码表示如下

    // 移动赋值
    string& operator= (string && s)
    {
      cout << "string& operatpr=(string&& s)" << endl;
      swap(s);
      return *this;
    }


移动赋值和赋值运算符重载的区别

  • 在没有增加移动赋值之前 赋值运算符重载是使用const左值引用来接受参数 因为无论赋值时传入的是左值还是右值 都会调用拷贝构造函数
  • 增加移动赋值之后 由于移动赋值采用的是右值引用来接受参数 因此如果移动赋值传入的是右值 那么就会调用移动赋值
  • 原本赋值时是调用拷贝构造进行了深拷贝 而移动赋值只需要调用swap函数进行资源转移即可 因此移动赋值的代价比赋值运算符重载小的很多


写了移动赋值之后我们刚刚写的代码就会变成两次swap了

78ab4f3234b148b2b64666e2cc2eb49b.png

STL中的容器

C++11标准出来之后 所有的STL容器都增加了移动构造和移动赋值

以string容器为例

移动构造

4d77c1a90fef40548f0d93f85f19d90e.png

移动赋值

0542e741d3fe453bbe13e2395994aed6.png


右值引用引用左值

在上面我们也说过了 右值引用不能直接引用左值 但是可以通过move函数间接的引用

move函数这个名字很具有迷惑性 实际上它并不能移动过任何值 它唯一的功能就是将一个左值强制转化为右值引用 然后实现移动语义

move函数的定义如下

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
  //forward _Arg as movable
  return ((typename remove_reference<_Ty>::type&&)_Arg);
}


这里有两点需要说明一下

  • move函数中_Arg参数的类型不是右值引用而是万能引用 万能引用和右值引用的形式一样 但是右值引用是需要确定的类型
  • 一个左值被move之后它的资源有可能被转移给别的数据了 所以说慎用被move后的左值


右值引用的其他使用场景

插入函数

C++11标准出来之后 STL中的容器除了增加移动构造和移动赋值之外 STL容器插入接口函数也增加了右值引用版本


我们这里以List容器的push_back函数为例

3be1f33f5b9c4ca5b8138640727be632.png右值引用版本插入函数的意义

如果list容器中储存的是string对象 那么在调用插入函数的时候 会有以下几种插入方式

  list<shy::string> ls;
  shy::string s("1111");
  ls.push_back(s);  // 拷贝构造
  ls.push_back("2222"); // 移动构造
  ls.push_back(shy::string("3333")); // 移动构造
  ls.push_back(std::move(s)); // 移动构造


我们可以发现 如果我们插入的是右值 那么就是调用移动构造进行资源转移

演示效果如下

4994fe8801db4267b3234d19cddc0c3b.png

在C++11之前list容器的push_back接口只有一个左值引用版本的插入 因此在构造节点的时候 左右值都只能匹配到string的拷贝构造函数进行深拷贝

5b6717c592a348f1970d6157fd2e001f.png

  1. 在C++11版本之后 string类提出了移动构造函数 此时如果传入的值是右值就会采用移动构造 这时候就会进行一个资源转移而不是拷贝构造
  2. 我们上面的例子中 传入的第一个参数是左值 后面的参数全部是右值 所以说第一次是拷贝构造 后面的全部是移动构造了
相关文章
|
28天前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
39 1
|
5月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
33 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
5月前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
29 2
|
5月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
34 1
|
4月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
42 0
|
5月前
|
存储 安全 程序员
C++11:右值引用
C++11:右值引用
37 0
|
5天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
15 4
|
5天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
15 4
|
5天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
10 1
|
15天前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)