【C++】右值引用和移动语义 | 新的类功能 | 可变参数模板(上)

简介: 【C++】右值引用和移动语义 | 新的类功能 | 可变参数模板(上)

👉左值引用和右值引用👈


左值引用和右值引用


传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。


什么是左值?什么是左值引用?


左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名


int main()
{
  // 以下的p、b、c、*p都是左值
  int* p = new int(0);
  int b = 1;
  const int c = 2;
  // 以下几个是对上面左值的左值引用
  int*& rp = p;
  int& rb = b;
  const int& rc = c;
  int& pvalue = *p;
  return 0;
}


什么是右值?什么是右值引用?


右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。


int main()
{
  double x = 1.1, y = 2.2;
  // 以下几个都是常见的右值
  10;
  x + y;
  fmin(x, y);
  // 以下几个都是对右值的右值引用
  int&& rr1 = 10;
  double&& rr2 = x + y;
  double&& rr3 = fmin(x, y);
  // 下面的语句都会编译报错,左操作数必须为左值
  //10 = 1;
  //x + y = 1;
  //fmin(x, y) = 1;
  return 0;
}


左值可以引用右值吗?右值可以引用左值吗?


// x既能接受左值,又能接受右值
template <class T>
void Func(const T& x)
{
  //...
}
int main()
{
  // 左值引用可以引用右值吗?const的左值引用可以
  double x = 1.1, y = 2.2;
  //double& rr1 = x + y;  // 编译报错
  const double& rr2 = x + y; // 可以
  // 右值引用可以引用左值吗?不可以,可以引用move以后的左值
  int a = 10;
  //int&& rr3 = a; // 编译报错
  int&& rr5 = move(a);
  return 0;
}


左值引用与右值引用总结:左值引用只能引用左值,不能引用右值。但是 const 左值引用既可引用左值,也可引用右值。右值引用只能引用右值,不能引用左值,但是右值引用可以 move 以后的左值。


需要注意的是:右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。也就是说,不能取字面量 10 的地址,但是 rr1 引用后,可以对 rr1 取地址,也可以修改 rr1。如果不想 rr1 被修改,可以用 const int&& rr1 去引用。注:rr1 和 rr2 都是左值。


int main()
{
  double x = 1.1, y = 2.2;
  int&& rr1 = 10;
  const double&& rr2 = x + y;
  rr1 = 20;
  //rr2 = 5.5; // 报错
  cout << &rr1 << endl;
  cout << &rr2 << endl;
  return 0;
}

8477ecf169524728a1edac36057e7be8.png


右值引用使用场景和意义


左值引用解决的问题:

  1. 做参数:a. 减少拷贝,提高效率。b. 做输出型参数
  2. 做返回值:a. 减少拷贝,提高效率。b. 引用返回,可以修改返回对象(比如:operator[]



左值引用既可以引用左值和又可以引用右值,那为什么C++11 还要提出右值引用呢?其实左值引用无法解决一些场景的问题,所以就提出了右值引用。

98f7f47c84644bd48d48c754d56301be.png


C++11 的右值引用的一个重要功能就是要解决上面的问题,但右值引用并不是直接作为返回值起作用的。

4e9a3a60cc39408a891b8874e816b4cd.png

18447b71c7ba449b8e75fcf5140b7035.png


注:只有在一个表达式里,编译器才能够优化,上图的场景无法优化。对象已经存在,只能用to_string生成的临时对象调用赋值运算符重载赋值给ret


那么在string类里添加移动构造和移动赋值就能够解决上面的问题了。


// 移动构造
string(string&& s)
  :_str(nullptr)
  , _size(0)
  , _capacity(0)
{
  cout << "string(string&& s) -- 移动语义" << endl;
  swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
  cout << "string& operator=(string&& s) -- 移动语义" << endl;
  swap(s);
  return *this;
}

d2ec46b38cf2491f904ea0d6b2bdaa5b.png


792bb98134ec40c8a8b591d3212487fe.png


9b6ae7d8a5644a1387e0ac06b0cdb4c2.png

4d045fa67b524869acb34d75258c9125.png


添加移动构造和移动赋值后,就不会存在深拷贝了。在bit::string中增加移动构造和移动赋值,移动构造和移动赋值本质是将参数右值的资源转移到指定的对象中,那就不需要深拷贝了。


注:右值有两类,第一类是纯右值,即内置类型右值;第二类是将亡值,即自定义类型右值。右值将亡值的资源可以转移到指定的对象,而左值不能。移动构造和移动赋值是延长了资源的生命周期。


4677c7878e19454a9ddd4d57ac09c559.png

完整代码


namespace Joy
{
  class string
  {
  public:
    typedef char* iterator;
    iterator begin()
    {
      return _str;
    }
    iterator end()
    {
      return _str + _size;
    }
    string(const char* str = "")
      :_size(strlen(str))
      , _capacity(_size)
    {
      //cout << "string(char* str)" << endl;
      _str = new char[_capacity + 1];
      strcpy(_str, str);
    }
    // s1.swap(s2)
    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(tmp);
      // 传统写法
      _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;
    }
    // 移动构造
    string(string&& s)
      :_str(nullptr)
      , _size(0)
      , _capacity(0)
    {
      cout << "string(string&& s) -- 移动语义" << endl;
      swap(s);
    }
    // 移动赋值
    string& operator=(string&& s)
    {
      cout << "string& operator=(string&& s) -- 移动语义" << endl;
      swap(s);
      return *this;
    }
    ~string()
    {
      delete[] _str;
      _str = nullptr;
    }
    char& operator[](size_t pos)
    {
      assert(pos < _size);
      return _str[pos];
    }
    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';
    }
    //string operator+=(char ch)
    string& operator+=(char ch)
    {
      push_back(ch);
      return *this;
    }
    const char* c_str() const
    {
      return _str;
    }
  private:
    char* _str;
    size_t _size;
    size_t _capacity; // 不包含最后做标识的\0
  };
  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 += ('0' + x);
    }
    if (flag == false)
    {
      str += '-';
    }
    reverse(str.begin(), str.end());  
    return str;
  }
}


STL 中的容器都是增加了移动构造和移动赋值的。


c50ce9e0b8464b719f8e2fc5b968cc72.png


STL 容器的插入接口函数也增加了右值引用版本。


e70140dff2cc49c8a3d1b2c58c869772.png

5587db13be48495d91bb51e5217df764.png

e7a899e77f9d4529912001d63b9bb9df.png

完美转发


模板中的 && 万能引用


void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既能引用左值,也能引用右值
// 引用折叠
template<typename T>
void PerfectForward(T&& t)
{
  Fun(t);
}
int main()
{
  PerfectForward(10); // 右值
  int a;
  PerfectForward(a); // 左值
  PerfectForward(std::move(a)); // 右值
  const int b = 8;
  PerfectForward(b); // const 左值
  PerfectForward(std::move(b)); // const 右值
  return 0;
}


854ee039ed624e14b6d8368a2b1a980c.png


可以看到,上面的引用通通被折叠成左值引用。其实这可以用上面的一个知识点来解释,当右值引用右值时,那么这个引用的属性也是左值,其有自己的地址。


std::forward 完美转发在传参的过程中保留对象原生类型属性。


void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既能引用左值,也能引用右值
template<typename T>
void PerfectForward(T&& t)
{
  Fun(std::forward<T>(t));
}
int main()
{
  PerfectForward(10); // 右值
  int a;
  PerfectForward(a); // 左值
  PerfectForward(std::move(a)); // 右值
  const int b = 8;
  PerfectForward(b); // const 左值
  PerfectForward(std::move(b)); // const 右值
  return 0;
}


2aa5b337cff741b2a52cdcab5e7270ec.png


注:如果想要一直保持对象的元素类型,就要一直完美转发。注:只有模板参数采用万能引用,确定的类型没有万能引用。

相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
5月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
191 0
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
149 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
228 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
278 12
|
8月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
8月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
8月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
8月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。