从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)

简介: 从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值

从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上):https://developer.aliyun.com/article/1522384


3.3 左值引用与右值引用比较

思考:左值引用可以引用右值吗?

       要知道,右值引用是C++11才出来的,右值传参给函数还是右值,那我们以前写的函数都用不了右值传参了?

template<class T>
void Func(const T& x)
{}

       这里去掉const肯定是不能传参的,为了给右值传参(当然还有其它原因),所以const的左值引用可以引用右值。总结:普通的左值引用不可以引用右值,const的左值引用可以引用右值。

思考:右值引用可以引用左值吗?

       右值引用不可以引用普通的左值,可以引用move以后的左值:(move这个语法先记住)左值经过move以后就变成了右值,如:

int main()
{
  // 左值引用可以引用右值吗? const的左值引用可以
  double x = 1.1, y = 2.2;
  //double& r1 = x + y;
  const double& r1 = x + y;
 
  // 右值引用可以引用左值吗?可以引用move以后的左值
  int b = 7;
  //int&& rr5 = b;
  int&& rr5 = move(b);
 
  return 0;
}

成功编译:

VS的提示已经很智能了:


3.4 右值引用的使用场景

namespace rtx
{
  class string
  {
  public:
    string(const char* str = "")
      :_size(strlen(str))
      , _capacity(_size)
    {
      _str = new char[_capacity + 1];
      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)
    {
      cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
 
      //string tmp(s._str);
      //swap(s);
 
      _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;
    }
 
  protected:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
}

       先自己实现一个string,只有拷贝构造函数,赋值运算符重载函数,析构函数,以及一个普通的构造函数。无论是拷贝构造还是赋值运算符重载,都会进行深拷贝,采用现代写法来实现:

namespace rtx
{
  class string
  {
  public:
    string(const char* str = "")
      :_size(strlen(str))
      , _capacity(_size)
    {
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }
 
    const char* c_str() const
    {
      return _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=(string s) -- 拷贝赋值(深拷贝)" << endl;
      string tmp(s);
      swap(tmp);
 
      return *this;
    }
 
    ~string()
    {
      delete[] _str;
      _str = nullptr;
    }
 
  protected:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
}

左值引用的场景:

使用普通传值调用,存在一次深拷贝:

void Func(rtx::string s)
{}
 
int main()
{
  rtx::string s("hello world");
  Func(s);
 
  return 0;
}


使用传拷贝引用时,不存在深拷贝,Func函数直接使用main函数中的s1对象:

void Func(rtx::string& s)
{}
 
int main()
{
  rtx::string s("hello world");
  Func(s);
 
  return 0;
}

函数返回参数和上面一样,传引用返回有时确实能提高效率。


3.4.1 左值引用的功能和短板

左值引用的功能:
做参数。

1. 减少拷贝,提高效率。

2. 做输出型参数。

做返回值。

1. 减少拷贝,提高效率。

2. 引用返回,可以修改返回对象(比如: operator[ ])。

但是左值引用做返回值只解决了70%的问题,在类似 to_string 函数中:

  • 传值返回时,存在一次深拷贝。
  • rtx::string to_string(int value)

要知道深拷贝的代价是比较大的,深拷贝次数减少可以很大程度上提高代码的效率。

  • 传左值引用返回时,不存在深拷贝。(可以吗?)
  • rtx::string& to_string(int value)

       但是敢传引用返回吗?我们把int value 转换成string,此时的 string 是一个形参。出了函数就销毁了。外面拿到的就是被销毁了的栈帧。

所以左值引用存在的短板:

       前面我们在调用 to_string 函数的时候,我们把int value 转换成string,此时的 string 是一个形参。所以只能传值返回,此时mian函数中拿到 to_string 中的 string 对象要进行两次深拷贝。


       第一次深拷贝,to_string函数返回时,会将string对象放在一个临时变量中,此时发生的深拷贝。函数返回时,如果是内置类型等几个字节的变量,会将函数中的临时变量放在寄存器中返回,如果是自定义类型所占空间比较大,就会放在临时变量中压栈到上一级栈帧中。


第二次深拷贝,main函数中,ret接收函数返回了的string对象时会再发生一次深拷贝。


       但是编译器会进行优化,将两次深拷贝优化成一次。虽然只有一次,但有些情况代价还是很大的。


       C++98是如何解决上面的问题?那就是输出型参数:rtx::string to_string(int value)变成rtx::void to_string(int value,string& s)。但是这样不太符合使用习惯。


有没有办法让它符合使用习惯,并且一次深拷贝都没有?那就要用到下面的C++11新增的移动构造和移动赋值了

3.4.2 移动构造

此时用右值引用就可以解决这个问题。

右值引用的价值之一:补齐临时对象不能传引用返回这个短板

前面的深拷贝是拷贝构造产生的:string(const string& s) // 拷贝构造(形参是左值引用)

演示在string类中增加一个移动构造函数:

前面提到过:内置类型的右值被称为纯右值

自定义类型的右值被称为将亡值。(这里的传右值就是将亡值

基于拷贝构造:无论是左值还是右值都老老实实地开空间:

    string(const string& s) // 拷贝构造
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
      string tmp(s._str);
      swap(tmp);
    } 

       左值因为还要使用,肯定要开空间的,这里的右值是将亡值,没用了,所以也不用开空间了,因为不用开空间了,所以深拷贝也没了,而是资源转移(直接swap):

    string(string&& s) // 移动构造
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(string&& s) -- 移动构造(资源转移)" << endl;
      swap(s);
    }
  • 移动构造的形参是右值引用。

从to_string中返回的string对象是一个临时变量,具有常性,也就是我们所说的右值。

  • 用右值来构造string对象时,会自定匹配移动构造函数。(以前没有移动构造时,右值传参会走拷贝构造,因为const 的左值引用可以接收右值,但是这不是最优方案,现在写了移动构造,右值传参就会走移动构造)

3.4.3 移动赋值

拷贝赋值移动赋值和拷贝构造移动构造类似:

    string& operator=(const string& s) // 拷贝赋值
    {
      cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
      string tmp(s);
      swap(tmp);
 
      return *this;
    }
 
    string& operator=(string&& s) // 移动赋值
    {
      cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
      swap(s);
 
      return *this;
    }

总结:右值引用和左值引用减少拷贝的原理不太一样。

  • 左值引用是别名,直接在原本的对象上起作用。
  • 右值引用是间接起作用,通过右值引用识别到右值,然后在移动构造和移动赋值中进行资源转移。

       使用移动构造和移动赋值时,被转移资源的对象必须是个将亡值(像to_string的使用一样),因为会被销毁。C++11的STL标准库中也提供了移动构造和移动赋值函数。

3.4.4 插入右值时减少深拷贝

C++11在STL库容器中的所有插入接口都提供了右值版本,push_back,insert等。

在我们写的string恢复这两个接口:

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

然后分别像库里的 list 插入左值和右值

1.int main()
{
  list<rtx::string> lt;
  rtx::string s1("hello"); // 左值
  lt.push_back(s1);  // 插入左值
 
  cout << "----------------------------------" << endl;
 
  lt.push_back(rtx::string("world")); // 插入右值
  //lt.push_back("world");
 
  return 0;
}

如果没有移动构造那么下面的也是深拷贝了。

从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下):https://developer.aliyun.com/article/1522395

目录
相关文章
|
13天前
|
存储 Java C++
【c++】list详细讲解
【c++】list详细讲解
15 5
|
13天前
|
Java C++ Python
【c++】list 模拟
【c++】list 模拟
9 1
|
21天前
|
存储 编译器 C语言
【C++】list模拟实现
本文档介绍了C++ STL中`list`容器的模拟实现,包括`ListNode`节点类、迭代器类和`list`类的详细设计。`ListNode`模板类存储数据并维护前后指针;`ListIterator`是一个复杂的模板类,提供解引用、自增/自减以及比较操作。`list`类包含了链表的各种操作,如插入、删除、访问元素等,并使用迭代器作为访问接口。实现中,迭代器不再是简单的指针,而是拥有完整功能的对象。此外,文档还提到了迭代器的实现对C++语法的特殊处理,使得`it-&gt;_val`的写法成为可能。文章通过分步骤展示`list`的各个组件的实现,帮助读者深入理解STL容器的内部工作原理。
|
21天前
|
算法 搜索推荐 C++
【C++】list的使用(下)
`C++` 中 `std::list` 的 `merge()`、`sort()` 和 `reverse()` 操作: - `merge(x)` 和 `merge(x, comp)`: 合并两个已排序的`list`,将`x`的元素按顺序插入当前`list`,`x`清空。比较可自定义。 - `sort()` 和 `sort(comp)`: 对`list`元素排序,保持等价元素相对顺序。内置排序基于稳定排序算法,速度较慢。 -reverse(): 反转`list`中元素的顺序。 这些操作不涉及元素构造/销毁,直接移动元素。注意,`sort()`不适合`std::list`,因链表结构不利于快速排序
|
21天前
|
C++ 容器
【C++】list的使用(下)
这篇博客探讨了C++ STL中`list`容器的几个关键操作,包括`splice()`、`remove()`、`remove_if()`和`unique()`。`splice()`允许高效地合并或移动`list`中的元素,无需构造或销毁。`remove()`根据值删除元素,而`remove_if()`则基于谓词移除元素。`unique()`则去除连续重复的元素,可选地使用自定义比较函数。每个操作都附带了代码示例以说明其用法。
|
21天前
|
编译器 C++ 容器
【C++】list的使用(上)
迭代器在STL中统一了访问接口,如`list`的`begin()`和`end()`。示例展示了如何使用正向和反向迭代器遍历`list`。注意`list`的迭代器不支持加减操作,只能用`++`和`--`。容器的`empty()`和`size()`用于检查状态和获取元素数。`front()`和`back()`访问首尾元素,`assign()`重载函数用于替换内容,`push_*/pop_*`管理两端元素,`insert()`插入元素,`erase()`删除元素,`resize()`调整大小,`clear()`清空容器。这些接口与`vector`和`string`类似,方便使用。
|
22天前
|
存储 C++
C++的list-map链表与映射表
```markdown C++ 中的`list`和`map`提供链表和映射表功能。`list`是双向链表,支持头尾插入删除(`push_front/push_back/pop_front/pop_back`),迭代器遍历及任意位置插入删除。`map`是键值对集合,自动按键排序,支持直接通过键来添加、修改和删除元素。两者均能使用范围for循环遍历,`map`的`count`函数用于统计键值出现次数。 ```
16 1
|
21天前
|
存储 编译器 C语言
【C++】list的使用(上)
**C++ STL的list是一个基于双向循环链表的容器,支持常数时间内插入和删除,但不支持随机访问。默认构造函数、填充构造、迭代器范围构造和拷贝构造提供多种初始化方式。析构函数自动释放内存,赋值运算符重载用于内容替换。示例代码展示了构造和赋值操作。**
|
21天前
|
存储 算法 程序员
C++基础知识(八:STL标准库(Vectors和list))
C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), lists(链表), 和 stacks(栈)等. STL容器的提供是为了让开发者可以更高效率的去开发,同时我们应该也需要知道他们的底层实现,这样在出现错误的时候我们才知道一些原因,才可以更好的去解决问题。