从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

目录
相关文章
|
2月前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
36 1
|
2月前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
58 7
|
2月前
|
存储 编译器 C++
C++ initializer_list&&类型推导
在 C++ 中,`initializer_list` 提供了一种方便的方式来初始化容器和传递参数,而右值引用则是实现高效资源管理和移动语义的关键特性。尽管在实际应用中 `initializer_list&&` 并不常见,但理解其类型推导和使用方式有助于深入掌握现代 C++ 的高级特性。
25 4
|
2月前
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
101 0
|
4月前
|
存储 算法 C++
【C++打怪之路Lv10】-- list
【C++打怪之路Lv10】-- list
29 1
|
4月前
|
存储 缓存 C++
C++番外篇——list与vector的比较
C++番外篇——list与vector的比较
35 0
|
4月前
|
C++
C++番外篇——list的实现
C++番外篇——list的实现
29 0
|
4月前
|
存储 C++ 容器
C++入门9——list的使用
C++入门9——list的使用
26 0
|
4月前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
44 0
|
25天前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
55 23