从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

目录
相关文章
|
6月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
243 26
|
安全 编译器 C语言
C++入门1——从C语言到C++的过渡
C++入门1——从C语言到C++的过渡
277 2
|
10月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
573 6
|
存储 编译器 C++
C++ initializer_list&&类型推导
在 C++ 中,`initializer_list` 提供了一种方便的方式来初始化容器和传递参数,而右值引用则是实现高效资源管理和移动语义的关键特性。尽管在实际应用中 `initializer_list&&` 并不常见,但理解其类型推导和使用方式有助于深入掌握现代 C++ 的高级特性。
197 4
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
362 10
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
514 0
|
算法 机器人 C语言
ROS仿真支持C++和C语言
ROS仿真支持C++和C语言
594 1
|
存储 算法 C语言
C语言手撕实战代码_二叉树_构造二叉树_层序遍历二叉树_二叉树深度的超详细代码实现
这段代码和文本介绍了一系列二叉树相关的问题及其解决方案。其中包括根据前序和中序序列构建二叉树、通过层次遍历序列和中序序列创建二叉树、计算二叉树节点数量、叶子节点数量、度为1的节点数量、二叉树高度、特定节点子树深度、判断两棵树是否相似、将叶子节点链接成双向链表、计算算术表达式的值、判断是否为完全二叉树以及求二叉树的最大宽度等。每道题目均提供了详细的算法思路及相应的C/C++代码实现,帮助读者理解和掌握二叉树的基本操作与应用。
424 2
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
220 0
|
编译器 C语言 C++
从C语言到C++
本文档详细介绍了C++相较于C语言的一些改进和新特性,包括类型检查、逻辑类型 `bool`、枚举类型、可赋值的表达式等。同时,文档还讲解了C++中的标准输入输出流 `cin` 和 `cout` 的使用方法及格式化输出技巧。此外,还介绍了函数重载、运算符重载、默认参数等高级特性,并探讨了引用的概念及其应用,包括常引用和引用的本质分析。以下是简要概述: 本文档适合有一定C语言基础的学习者深入了解C++的新特性及其应用。