• C++ 移动语义


    C++11 移动语义

    首先,移动语义和完美转发这两个概念是在C++的模板编程的基础上,新增的特性,主要是配合模板来使用。本篇会从C++的值类型,到移动拷贝与移动赋值来理解移动语义与完美转发。

    C++的值类型

    我们知道,每个变量都有类型,或整形或字符型等来进行了分类,不仅如此,C++表达式(带有操作数的操作符、字面量、变量名等)在类型的属性上,还有一种属性,即值类别(value category)。且每个表达式只属于三种基本值尖别中的一种:左值(lvalue),右值(rvalue),将亡值(xvalue),每个值类别都与某种引用类型对应。
    在这里插入图片描述
    其中,左值和将亡值成为泛左值(generalized value,gvalue),纯右值和将亡值合称为右值(right value,rvalue)。

    一般我们讲,左值就是可以取地址的,具有名字的,比如 int a; a是变量的名字,&a是变量的地址,a就是左值。那么右值呢,自然就是不可以取地址的,比如int b=10; 而这个10就是一个右值,在内存中不会分配有地址,自然也不能取地址。
    将亡值,则是指在调用某个函数退出返回时,如果函数有返回值,那么就会有将亡值的存在,为什么称之为将亡值,就是说这个值在函数作用域创建,但由于函数返回结束,局部变量都会销毁,故会产生一个将亡值来接收这个值,完成赋值的任务。

    从上图也可以看出,将亡值既可能转为左值,也可能成为右值,那么关键就在于要看是否具有名字了。

    下面看这样一段程序:

    #include
    #include
    using namespace std;
    
    class MyString
    {
    private:
    	char* str; // heap;
    public:
    	MyString(const char* p = nullptr) :str(nullptr)
    	{
    		if (p != nullptr)
    		{
    			int n = strlen(p) + 1;
    			str = new char[n];
    			strcpy_s(str, n, p);
    		}
    		cout << "Create MyString: " << this << endl;
    	}
    	MyString(const MyString& st)
    	{
    		if(st.str!=NULL)
    		str = st.str;
    		cout << "Copy Create MyString: " << this << endl;
    	}
    	MyString& operator=(const MyString& st)
    	{
    		if (st.str != NULL)
    		str = st.str;
    		cout << this << " operator=(const MyString &):  " << &st << endl;
    		return *this;
    	}
    	~MyString()
    	{
    		delete[]str;
    		str = nullptr;
    		cout << "Destroy MyString : " << this << endl;
    	}
    	void PrintString() const
    	{
    		if (str != nullptr)
    		{
    			cout << str << endl;
    		}
    	}
    };
    
    
    int main()
    {
    	MyString *a=new MyString("lisa");
    	MyString *b = a;
    	delete b;
    	a->PrintString();
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    MyString类型成员有指针变量,且采用浅拷贝方式。当程序运行时,可以看到,两个指针指向了同一个地址,此时,若释放了b指针,再以a指针访问指针成员,就会出现问题。
    在这里插入图片描述
    还有,当函数以值类型返回,构造临时对象,若有指针变量,且采用浅拷贝,就会出现多次析构的问题,导致程序崩溃。

    当我们将程序都改为深拷贝时,深拷贝又会导致,程序多次骚扰对空间,此时就提出了move语义。

    std::move
    std::move其实并没有移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,move基本等同于一个类型转换。

    值得注意的是,通过move转化成右值后,被转化的左值的生命周期并没有随着左右值的转化而改变。但通常情况下,我们需要转换成右值引用的还是一个确定生命期即将结束的对象。


    右值引用与移动构造和移动赋值

    在c++11中增加了右值引用的概念,即对右值的引用,通过右值引用,可以延长右值的生命期。我们都知道左值引用是变量值的别名,那么右值引用则是不具名变量的别名。

    右值引用是不能绑定到任何左值的,但有个例外,常量左值是一个万能引用,可以引用任何值,包括右值引用。

    class MyString
    {
    private:
    	char* str; // heap;
    public:
    	MyString(const char* p = nullptr) :str(nullptr)
    	{
    		if (p != nullptr)
    		{
    			int n = strlen(p) + 1;
    			str = new char[n];
    			strcpy_s(str, n, p);
    		}
    		cout << "Create MyString: " << this << endl;
    	}
    	MyString(const MyString& st)
    	{
    		if (st.str != nullptr)
    		{
    			int n = strlen(st.str) + 1;
    			str = new char[n];
    			strcpy_s(str, n, st.str);
    		}
    		cout << "Copy Create MyString: " << this << endl;
    	}
    	MyString& operator=(const MyString& st)
    	{
    		if (this != &st && str != st.str)
    		{
    			delete[]str;
    			if (st.str != nullptr)
    			{
    				int n = strlen(st.str) + 1;
    				str = new char[n];
    				strcpy_s(str, n, st.str);
    			}
    		}
    		cout << this << " operator=(const MyString &):  " << &st << endl;
    		return *this;
    	}
    	MyString(MyString&& st)
    	{
    		str = st.str;
    		st.str = nullptr;
    		cout << "Move Copy Create MyString" << this << endl;
    	}
    	MyString& operator=(MyString&& st)
    	{
    		if (this == &st) return *this;
    		if (this->str == st.str)
    		{
    			st.str = nullptr;
    			return *this;
    		}
    		delete[]str;
    		str = st.str;
    		st.str = nullptr;
    		cout << "Move operator=(MyString &&)" << endl;
    		return *this;
    	}
    	~MyString()
    	{
    		delete[]str;
    		str = nullptr;
    		cout << "Destroy MyString : " << this << endl;
    	}
    	void PrintString() const
    	{
    		if (str != nullptr)
    		{
    			cout << str << endl;
    		}
    	}
    };
    
    int main()
    {
    	const MyString stra("hello");
    	MyString strb;
    	
    	strb = std::move(stra);//调用普通的赋值方法
    	strb.PrintString();
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85

    在这里插入图片描述
    这里的move还是调用普通的赋值函数,并未做到真正的资源转移,但是若写成如下结构:

    int main()
    {
    	const MyString stra("hello");
    	MyString strb;
    	
    	//strb = std::move(stra);//调用普通的赋值方法
    	strb = (MyString&&)stra;
    	strb.PrintString();
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    通过右值引用,可以延长右值的生命期。从而,有了右值引用出现,这个时候配合移动构造与移动赋值,就可以完成资源转移了。

    然后,我们再看一个例子:

    MyString& fun()
    {
    	MyString st=("newdata");
    	return st;//xvalue
    }
    
    int main()
    {
    	MyString("zhangsan").PrintString();
    
    	const MyString& a = fun();
    	a.PrintString();
    
    	MyString& b = fun();
    	b.PrintString();
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在程序运行时,会发现程序崩溃了,原因是:
    在这里插入图片描述
    函数中返回局部对象的引用,因为函数调用结束会销毁局部对象,而引用则就成为了非法的访问。因为不要在函数中返回局部对象的引用。

    若我们将fun()函数的返回改为右值引用呢?

    MyString&& fun()
    {
    	return MyString("newdata");
    }
    
    int main()
    {
    	MyString("zhangsan").PrintString();
    
    	const MyString& a = fun();//x
    	a.PrintString();
    
    	//MyString& b = fun();
    	//b.PrintString();
    	
    	MyString&& c = fun();//x
    	c.PrintString();
    
    	MyString&& d = c;//error
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    将亡值回去的时候,就得看看有没有具名,一旦具名就是左值了,否则是右值

    在这里插入图片描述
    可以发现,右值引用是不具名的,但是右值引用本身却是个左值,经过右值引用b接收后,就已经变成了左值,具有了名字。

  • 相关阅读:
    windows配置FTP服务
    php网盘程序使用php网盘程序
    平衡二叉搜索树--AVL树
    第二证券|券商12月金股出炉!多只地产股成热门,科创仍是中长期主线
    用 Pytest+Allure 生成漂亮的 HTML 图形化测试报告
    CML、LVPECL和LVDS
    【RocketMQ】DLedger模式下的选主流程分析
    关于蓝牙人员定位的几个重要问题
    Win11找不到DNS地址怎么办?Win11找不到DNS无法访问网页解决方法
    【愚公系列】2022年10月 LiteDB数据库-.Net Core中的使用
  • 原文地址:https://blog.csdn.net/zypf_lover/article/details/126423979