从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

目录
相关文章
|
16天前
|
编译器 C++
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
C++进阶之路:何为运算符重载、赋值运算符重载与前后置++重载(类与对象_中篇)
24 1
|
8天前
|
C语言 C++ 编译器
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
【C++语言】冲突-C语言:输入输出、缺省参数、引用、内联函数
|
7天前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
|
8天前
|
C语言 C++
【C++语言】冲突-C语言:命名空间
【C++语言】冲突-C语言:命名空间
|
15天前
|
程序员 C语言 C++
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
C语言学习记录——动态内存习题(经典的笔试题)、C/C++中程序内存区域划分
19 0
|
23天前
|
安全 Linux 编译器
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(下)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
23 0
|
23天前
|
安全 C语言 C++
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(中)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
23 0
|
23天前
|
Linux 调度 C语言
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(上)
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
25 0
|
1天前
|
C++
C++一分钟之-类与对象初步
【6月更文挑战第20天】C++的类是对象的蓝图,封装数据和操作。对象是类的实例。关注访问权限、构造析构函数的使用,以及内存管理(深拷贝VS浅拷贝)。示例展示了如何创建和使用`Point`类对象。通过实践和理解原理,掌握面向对象编程基础。
29 2
C++一分钟之-类与对象初步
|
2天前
|
存储 编译器 C++