右值引用、移动语义、完美转发

右值引用、移动语义、完美转发

  • 左值、右值:在c++ 中,所有的值不是左值,就是右值。有名字的对象都是左值右值没有名字。还有一个可以区分左值右值的方法:看能不能对表达式取地址,如果能,则为左值,否则为右值

    • 左值:即表达式结束后依然存在的持久化对象

    • 右值:即表达式结束后就不再存在的临时对象

      • C++ 11扩展了右值的概念,将右值分为了纯右值和将亡值。

        • 纯右值:

          1. 非引用返回的临时变量
          2. 运算表达式产生的结果
          3. 字面常亮(C风格字符串除外,它是地址)
        • **将亡值:**与右值引用相关的表达式(C++ 11 为优化性能,减少空间的分配,将要释放的资源,延长其生命周期,而定义出来的新值)。列如:将要被移动的对象、T&&函数返回的值、std::move() 的返回值、转换成T&&的类型的转换函数的返回值。

          class Person{
              int m_nNum;
          }
          Person getTemp(){
              return Person();
          }
          int main(){
              int iTemp0=3;/*iTemp0 为左值,有地址。3为右值*/
              int iTemp1=iTemp0+8; //iTemp1 为左值,iTemp0+8 为右值,表达式返回值为临时对象
              Person person=getTemp(); //person 为左值,getTemp()的返回对象为右值 临时对象。
          }
          
  • **左值引用、右值引用:**C++ 98 中的引用很常见,就是给变量取个别名,在C++ 11 中,因为增加了右值引用的概念,所以C++ 98中的引用都称为左值引用。右值引用就是给右值取个别名,使用的符号是&&。

    • 语法: 数据类型&& 变量名=右值

      class Person {
      public:
          int m_Num = 0;
      };
      Person getPerson() {
          return Person();
      }
      
      int main(){
      
          std::cout<<"hello World"<
    • 引入右值引用的主要目的是为了实现移动语义。

      • 左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译器就会失败。但是,常量左值引用却是一个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值的生命周期延长。

        int &&A=3;//编译器通过				
        int&A=3; //编译器不通过  “初始化”: 无法从“int”转换为“int &”
        
        const int &A=3; //编译器通过 
        int a=1;
        const int &ra=a;//编译器通过 
        const int b=1;	//编译器通过 
        const int &rb=b;//编译器通过 
        
        

        注意:上述代码中,常量左值引用用在函数参数中,可以接受字面值的原因,可能会问,为什么常量左值引用可以这样?答案很简单,编译器做了特殊处理,就行右值引用一样。都是做了特殊处理。

  • **移动语义:**如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。这里深拷贝与浅拷贝区别,就不一一介绍。感兴趣的同学可以自行百度一下。回到题点,如果被拷贝对象是临时对象,拷贝完了就没什么用,这样会造成没有意义的资源申请和释放操作,如果拷贝的对象数据量比较大,这种资源的操作会极大的降低我们程序的性能。为了节省资源的申请和释放的时间,C++ 11 新增了移动语义。

    • 实现移动语义的主要两个函数:移动构造函数、移动赋值函数

      • **移动构造函数语法:**ClassName(ClassName&& other){}

      • **移动赋值函数语法:**ClassName &operator=(类名&&other){}

        class Person {
        public:
            int* m_data = nullptr;
            Person() = default;
            void alloc()  {
                m_data = new int;
                memset(m_data, 0, sizeof(int));
            }
            Person(const Person& other) {
                cout << "调用了深拷贝构造函数。 \n";
                if (m_data==nullptr)
                {
                    alloc();
                }
                memcpy(m_data, other.m_data, sizeof(int));
            }
            Person(Person&& other) {
        		cout << "调用了移动构造函数。 \n";
        		if (m_data != nullptr)
        		{
                    delete m_data;
        		}
                m_data = other.m_data;
                other.m_data = nullptr;
        	}
            Person& operator=(const Person& other) {
                cout << "调用了赋值函数。 \n";
                if (this == &other) {
                    return *this;
                }
                if (m_data == nullptr) {
                    alloc();
                }
                memcpy(m_data, other.m_data, sizeof(int));
            }
        
        
        	Person& operator=(Person&& other) {
        		cout << "调用了移动赋值函数。 \n";
        		if (this == &other) {
        			return *this;
        		}
        		if (m_data != nullptr)
        		{
        			delete m_data;
        		}
        		m_data = other.m_data;
        		other.m_data = nullptr;
        	}
        
            ~Person() {
                if (m_data != nullptr) {
                    delete m_data;
                    m_data = nullptr;
                }
            }
        };
        Person getPerson() {
            return Person();
        }
        
        int main(){
        
            Person person1;
            person1.alloc();
            *person1.m_data = 3;
            cout << "person.m_data=" << *person1.m_data << endl;
            
            Person person2 = person1;//调用拷贝构造函数
            cout << "person2.m_data=" << *person2.m_data << endl;
        
            Person person3;
            person3= person1;//调用赋值函数
        	cout << "person3.m_data=" << *person3.m_data << endl;
        
        
            return 0;
        }
        
        
        
        

        输出结果:
        person.m_data=3
        调用了深拷贝构造函数。
        person2.m_data=3
        调用了赋值函数。
        person3.m_data=3

      • 下面我们修改下main 函数让其调其移动构造函数、以及移动赋值函数

        int main(){
        
            Person person1;
            person1.alloc();
            *person1.m_data = 3;
            cout << "person.m_data=" << *person1.m_data << endl;
            
            Person person2 = person1;//调用拷贝构造函数
            cout << "person2.m_data=" << *person2.m_data << endl;
        
            Person person3;
            person3= person1;//调用赋值函数
        	cout << "person3.m_data=" << *person3.m_data << endl;
        
            Person  person4= std::move(person3);     //调用移动构造函数
            cout << "person4.m_data=" << *person4.m_data << endl;
        
            Person person5;
            person5 = std::move(person3);    //调用移动赋值函数
        	cout << "person5.m_data=" << *person5.m_data << endl;
        
            return 0;
        }
        
        

        person.m_data=3
        调用了深拷贝构造函数。
        person2.m_data=3
        调用了赋值函数。
        person3.m_data=3
        调用了移动构造函数。
        person4.m_data=3
        调用了移动赋值函数。
        person5.m_data=

        程序崩溃

        为什么奔溃?这里很多同学应该能猜到了吧!这是因为person3 被移动了两次,第一次移动,没有任何问题,但在第二次移动的时候,由于person3 的资源已经被移动了,所以在下面的调用访问,访问到空指针。所以导致崩溃。

      • 对于一个左值,会调用拷贝构造函数但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++ 11 就提出std::move()方法来将左值转义为右值。从而方便了移动语义。它其实就是告诉编译器,进行特殊处理。左值对象被转移资源后,不会立刻析构,只有在离开作用域后才会析构,如果此时继续使用左值的资源,可能会发生意想不到的错误。

      • 如果没有提供移动构造函数、移动赋值函数,只提供了拷贝构造函数、赋值函数,编译器找不到移动构造函数和移动赋值函数,就去找拷贝构造函数和赋值函数。

      • C++ 11 中所有容器都实现了移动语义,避免了对含有资源的对象发生无畏的拷贝。

      • 移动语义对于拥有资源(内存、文件句柄等)的对象有效,如果是基本类型,使用移动语义没有任何意义。

      • 其实说白了点就是std::move() 其实就是调用了C++ 的指针强制转换。下面就是std::move的源码

        template 
        _NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
            return static_cast&&>(_Arg);
        
  • **完美转发:**在函数模板中,可以将参数“完美”的转发给其他函数,所谓完美转发,即不仅能准确的转发参数的值,还能转发参数的左值和右值的属性。

    • 如果模板中函数的参数写成T&& 形式,那么函数既可以接收左值又可以接收右值。

    • 提供了模板函数std::forward(),用于转发参数,保持其值的属性。

      template
      void func(TT&&i){
          
          func1(std::forward(i));
      }
      

你可能感兴趣的:(C++,c++,算法,数据结构)