• C++11 右值引用和移动语义


    前言

    因为工作室要求写技术博客记录学习到的知识点,自己之前是没有写过博客的,所以现在用一篇介绍右值引用和移动语义的博客作为博客的第一篇,可能对于移动语义的理解还不够深刻,但可以作为一个简单的介绍博客

    右值引用

    要理解好右值引用首先要知道什么是左值?什么是右值?

    1.左值是表达式结束后依然存在的持久化对象
    2.右值则是表达式结束时就不再存在的值

    便捷区别方法:对表达式取地址,如果能,是左值,否则是右值

    int a=10; 
    int b=5; //这是一个左值
    &(a+b);    //这是一个右值
    
    //区别方法
    &a       //因为a是个左值,可以对其取地址
    &(a+b)  //因为(a+b)是个临时变量,是右值,并不能对其取地址
    

    下面先来对左值引用作一些介绍

    由于修饰符不同可以分为非常量左值和常量左值

    1.非常量左值只能绑定左值
    2.常量左值是个奇葩,因为他不仅可以绑定左值(非常量和常量),也能够绑定右值
    为什么它能够绑定右值呢?
    在于他们绑定了右值后,延长了右值的生命周期,使之像一个左值一样,但有一个东西是只能读不能改
    int a=10;       
    int c=10;//非常量左值
    const int b=5;  //常量左值
    
    int& d=a; //可以
    int& d=a+c; //报错 a+b是右值
    int& d=b;   //报错 b是常量左值
    
    const int& e=a;
    const int& e=b;
    const int& e=a+c; 
    //上面的三条式子都不会报错,因为常量左值都可以绑定
    

    在c++11中添加了右值引用的概念,用&&来表示右值引用

    原本右值是个临时变量,表达式结束时其生命也就结束了,但可以通过右值引用使其被存储到特定的位置,且可以获得该位置的地址

    换句说:左值引用可以比作一个拥有姓名的人给其取个别名,而右值引用则是一个没有姓名的人然后给其取个别名

    int a=10;
    int b=10;
    int&& c=a+b; //一个简单的右值引用
    

    为什么要引入右值引用呢?目的之一则是下面要讲到的移动语义的实现

    移动语义是实际文件还留在原来的地方,而只修改记录.

    用下面一个Useless类来说明(具体实现就不说了)

    #ifndef __USELESS_H
    #define __USELESS_H
    
    #include<iostream>
    
    class Useless
    {
    public:
    	Useless();
        Useless(int k);
    	Useless(int k, char ch);
    	Useless(const Useless& f);  //复制构造函数
    	Useless(Useless&& f);       //移动构造函数
    	~Useless(); 
    	Useless operator+(const Useless& f) const;
    	void ShowData() const;
    private:
    	int n; 
    	char* pc;
    	static int ct;   //统计对象的数量
    	void ShowObject() const;
    };
    
    
    #endif // __USELESS_H
    
    #include "Useless.h"
    
    int Useless::ct = 0;
    Useless::Useless()
    {
    	++ct;
    	n = 0;
    	pc = nullptr;
    	std::cout << "default construct called;number of object: " << ct << std::endl;
    	ShowObject();
    }
    
    Useless::Useless(int k): n(k)
    {
    	++ct;
    	std::cout << "int construct called;number of objects: " << ct << std::endl;
    	pc = new char[n];
    	ShowObject();
    }
    
    Useless::Useless(int k, char ch):n(k)
    {
    	++ct;
    	std::cout << "int char construct called;number of objects: " << ct << std::endl;
    	pc = new char[n];
    	for (int i = 0; i < n; i++)
    		pc[i] = ch;
    	ShowObject();
    }
    
    Useless::Useless(const Useless & f):n(f.n)
    {
    	++ct;
    	std::cout << "copy const called;number of objects: " << ct << std::endl;
    	pc = new char[n];
    	for (int i = 0; i < n; i++)
    		pc[i] = f.pc[i];
    	ShowObject();
    }
    
    Useless::Useless(Useless && f):n(f.n)
    {
    	++ct;
    	std::cout << " move construct called;number of objects: " << ct << std::endl;
    	pc = f.pc;
    	f.pc = nullptr; //give old object nothing in return 
    	f.n = 0;
    	ShowObject();
    }
    
    Useless::~Useless()
    {
    	std::cout << "destructor called;number of objects: " << --ct << std::endl;
    	std::cout << "deleted object:
    ";
    	ShowObject();
    	delete[] pc;
    }
    
    Useless Useless::operator+(const Useless & f) const
    {
    	std::cout << "Entering operator+()
    ";
    	Useless temp = Useless(n + f.n);
    	for (int i = 0; i < n; i++)
    		temp.pc[i] = pc[i];
    	for (int i = n; i < temp.n; i++)
    		temp.pc[i] = f.pc[i - n];
    	std::cout << "temp object:
    ";
    	std::cout << "Leaving operator+()
    ";
    	return temp;
    }
    
    void Useless::ShowData() const
    {
    	if (n==0)
    	{
    		std::cout << "(object empty)";
    	}
    	else
    	{
    		for (int i = 0; i < n; i++)
    			std::cout << pc[i];
    	}
    	std::cout << std::endl;
    }
    
    void Useless::ShowObject() const
    {
    	std::cout << "Number of elements: " << n;
    	std::cout << " Data address: " << (void *)pc << std::endl;
    }
    
    
    #include"Useless.h"
    #include<utility>
    
    int main()
    {
    	{
    		Useless one(10, 'x');
    		Useless two = one;  //深拷贝
    		Useless three(20, '0');
    		Useless four(one + three);  //operator+()
    		std::cout << "object one: ";
    		one.ShowData();
    		std::cout << "object two: ";
    		two.ShowData();
    		std::cout << "object three: ";
    		three.ShowData();
    		std::cout << "object four: ";
    		four.ShowData();
    	}
    	system("pause");
    	return 0;
    }
    
    首先看下复制构造函数
    Useless two = one;把one对象(左值)赋给了two,在这期间会调用复制构造函数Useless(const Useless& f)来实现复制;
    再来看看Useless four(one + three)这条语句,one+three会调用operatoe+()来产出了一个右值,这个右值作为参数调用了Useless(Useless && f)这个构造函数,
    我们称其为移动构造函数(你没有提供的话系统会提供一个默认的移动构造函数)

    现在再深入探究下一些函数的具体实现:

    一丶首先把移动构造函数的声明和定义注释掉

    1.首先参数one+three里面调用一个构造函数创建一个temp对象,然后通过一个复制构造函数来创建一个临时复制对象,然后指向了这个对象(即函数返回了这个临时对象);接下来,删除这个temp对象;

    Useless Useless::operator+(const Useless & f) const
    {
        ......
    	Useless temp = Useless(n + f.n);
    	for (int i = 0; i < n; i++)
    		temp.pc[i] = pc[i];
    	for (int i = n; i < temp.n; i++)
    		temp.pc[i] = f.pc[i - n];
        ......
    	return temp;
    }
    

    2.因为常量左值引用可以绑定右值,所以会调用下面这个函数

    Useless::Useless(const Useless & f):n(f.n)
    {
    	.....
    	pc = new char[n];
    	for (int i = 0; i < n; i++)
    		pc[i] = f.pc[i];
    	.......
    }
    

    所以会新建一个four对象.使其使用这个临时对象中的内存,接下来删除了这个临时对象

    这个表明了总共创建了三个对象

    这里可以调用了复制构造函数,但其只调用了一次

    因为编译器会优化返回值,然后在编译时加上-fno-elide-constructors选项即可关闭返回值优化
    在GCC编译时关闭这个返回值可以看到其调用了两次复制构造函数

    二丶把移动构造函数的声明和定义的注释取消掉

    1.这个one+three同样调用了operator+()这个函数

    2.(1)因为参数one+three是个临时对象,是个右值,所以编译器会先调用移动构造函数而不是复制构造函数

    (2)与复制构造函数不同的是,他直接指向了这个临时对象,然后把原来指向这个临时对象的指针改为nullptr(相当于一个对象的所有权的转移,把临时对象的所以权夺了过来)

    (3)把原来指向临时对象的指针改为nullptr是让这个对象析构不会使原来的内存被释放掉,不然这个移动构造函数没有意义

    这样便省去了复制构造函数中创建一个临时对象的过程,总共创建了两个对象;

    可以看到调用了一次移动构造函数,而一些无关的工作量减少

    而移动语义目的之一就是消除这些额外的工作.

    一旦工作量变大,就会导致一些额外的资源申请和释放的操作;使用移动语义就既能够够节省资源,也能够节省时间

    当然除了移动构造函数,还有移动赋值函数(在这里就不讲了,大致原理是一样的

    std::move()

    std::move()在头文件utility

    有时候左值是一个局部变量,即表明他也是有临时的生命周期,那么能不能也是调用移动语义而不是复制语义呢?

    c++11提供了std::move()来解决这个问题,他可以把左值强制转化为右值引用,使左值可用于移动语义中

    Useless four(one);  //调用复制构造函数
    Useless four(std::move(one));  //调用移动构造函数 
    

    如果我们没有提供移动构造函数,std::move()会失效但不会报错,因为会调用复制构造函数(const &)

    可能有待续写......

    作者:Ligo丶

    出处:https://www.cnblogs.com/Ligo-Z/

    本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

  • 相关阅读:
    android开发之重写Application类
    android开发之Parcelable使用详解
    android开发之Bundle使用
    android开发之gridlayout使用入门
    android开发之merge结合include优化布局
    android开发布局优化之ViewStub
    android开发之PreferenceScreen使用详解
    android开发之使用Messenger实现service与activity交互
    LeetCode All in One 题目讲解汇总(持续更新中...)
    JavaWeb知识点总结
  • 原文地址:https://www.cnblogs.com/Ligo-Z/p/11149587.html
Copyright © 2020-2023  润新知