[C++]右值引用和移动语义

目录

前言:

1 左值和右值的认识

2 左值引用和右值引用的区别

2.1 左值引用

2.2 右值引用

3 移动语义

3.1 移动构造

3.2 移动赋值


前言:

        本篇文章讲解了关于左值引用和右值引用的区别,以及为什么要有右值引用,并展示了右值引用的实际应用等。

1 左值和右值的认识

        如果大家提前不知道什么时左值,什么是右值的话应该会怎么判断呢?是不是左值就是放在左边的值,右值就是放在右边的值?答案很明显不会是这样的,简单举一个例子:

    int x = 1;
    int y = x;
    x + y = 1;

        请问我们的int y = x中的x是左值还是右值? x+y = 1的x+y是左值还是右值?

        答案就是x是左值,而x+y是右值,当然实际上是不能写为x+y=1的,我这里只是为了方便演示。那么为什么我就能这么笃定x是左值,而x+y是右值呢?

        事实上,对于左值和右值我们有一个很简单的判断方式,那就是右值是不会被实际开辟空间的,也就表示右值是无法被获取到地址的,如下:

         通过编译所报的错误也表明了取地址&符号只能获取到左值的地址,右值是不行的。

        我先给大家展示一些简单的右值引用操作:

    double x = 1, y = 2;
    double&& rr1 = 10; 
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);

        当然,我相信大家看到了这样的代码肯定有一个想法,这右值引用有啥用?感觉和左值引用没有什么区别啊?就是用了两个&&符号?

        大家有这样的疑问并没有任何的问题,因为右值引用本来也就不是这样用的,我只是带大家看一下而已,后面会讲解实际的引用场景。

2 左值引用和右值引用的区别

2.1 左值引用

        看到下方的代码:请问下面的代码能够编译成功嘛?

int get_num(int& num)
{
	return num;
}

void test2()
{
	int x = 1, y = 2;
	get_num(x + y);
}

int main()
{
    test2();
    return 0;
}

编译结果:

         左值引用并不能引用右值,但是只要我们在左值引用前面加一个const就能行了。

int get_num(const int& num)
{
    return num;
}

         请问,为什么加上const之后就能够编译通过了呢?咱们先从语义了解以下,其实对于右值来说就是一个临时的变量,它是有常量特性的,所以只要我们加上const是能够接收的,但是这并不能说通,因为我之前说过右值是没有实际地址的,所以就算加上const也不应该能行的。

        这确实是一个很好的问题,但是大家有没有想到那就是右值引用是什么时候出来的产物?C++11,那么中间那么长的一段时间都不支持const 左值引用接收右值嘛?

        如果不能接收,那么你又如何解释string类,里面的拷贝构造呢?

        // 拷贝构造
        string(const string& s)
            :_str(nullptr)
        {
            cout << "string(const string& s) -- 深拷贝" << endl;
            string tmp(s._str);
            swap(tmp);
        }

         我可是可以通过这样的方式来传值的哦:

    yf::string s1("hello world");
    yf::string s2(s1 + '!');

        我的s1+'!'可是一个右值哦,在C++11之前是没有右值引用的,如果const string& 无法接收,那么又怎么做呢?没办法,带有const 的左值引用必须支持右值的传参。

2.2 右值引用

        看了上面的左值接收右值,那么右值可以接收左值嘛?请看下方代码:

void test3()
{
	int s1 = 1;

	int&& s2 = s1;
}

         看来是不行的呢,为什么呢?其实右值涉及到了一个资源释放的问题,马上为大家讲解。

void test3()
{
	int s1 = 1;

	/*int&& s2 = s1;*/

	int&& s3 = move(s1);
}

        我们将左值用move移动之后就可以给右值引用了。

        其实右值我们也常常称它为将亡值,什么意思呢?也就是快要被销毁的数据,所以通常如果我们不管他的话,右值是使用完了就会被销毁。

        但是通过右值引用接收它,它的资源就被交换过来了,网上常常有人说右值引用是延长了变量的生命周期,其实这样的说法并不准确,因为右值这个对象、变量的确是被销毁了,但是我们通过右值引用的方式将他的资源转移过来了,也就是对象的资源的生命周期得到了延长而不是对象本身。相当于我们死了,但是腰子还能给别人用,差不多就是这样。

        关于移动还有一个非常好玩的东西,我先为大家展示一下,原理我之后为大家讲解:

         我们创建了两个string变量,一个有值,另一个为空,这没问题,然后我们打算通过赋值的方式将s1的值给s2,请看发生了什么事情:

         好家伙,有偷子,s1表示,我让你拷贝一份你就给我偷了?s2也很无语哇,说,你都move成为右值,你不是不要了嘛,我拿你的怎么了?

         它们谁有问题?没问题,我的锅,我不该这么写。那我就先给一个结论,当该类型支持移动语义的时候,通过move将变量变为右值之后,就会发生这样的事情。

3 移动语义

        既然上节中带大家见了移动语义会偷东西,那现在就让博主为大家见一下原理吧,对于stl库里面的东西,我们无法直接看到底层,所以博主简易实现了一个string类:

#pragma once
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;


namespace yf
{
	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);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		string(string&& s) noexcept
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s) noexcept
		{
			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;
		}

		string operator+(char ch)
		{
			string temp(*this);
			temp += ch;

			return temp;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

	yf::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		yf::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}

		return str;
	}
}

3.1 移动构造

        // 移动构造
        string(string&& s) noexcept
            :_str(nullptr)
            , _size(0)
            , _capacity(0)
        {
            cout << "string(string&& s) -- 移动语义" << endl;
            swap(s);
        }

         看到我们移动构造的写法了嘛?中间的函数体里面的内容直接对s进行了交换,并且我们传入的参数也是一个右值,也就表示了我们不仅能够拿到右值,还能将它的资源拿走,避免了二次拷贝的情况发生。

        正常情况下,我们的右值一般不是通过move左值过来的,而是本身就是一个右值,但是对于我们来说却有一些问题了,我们通过右值传参,其实已经是拿到了我们期望的数据了,也就是数据本身是已经计算完成了的,但是由于右值的属性,我们不能对这部分资源进行更改,只能读取这一部分的内容,然后又拷贝一次,这无疑是会让我们的运行效率变低,所以,如果我们能够直接拿到右值的资源,不再又一次的拷贝,效率肯定会提高的。请看下图资源转移过程:

yf::string s1("hello");

yf::string s2(s1 + '!');

        s1+'!'是一个右值,但是在此之前,编译器回去计算s1+'!'的值:

         计算完成之后,我们看到temp的资源直接被转换到了s下面,这有问题吗?没问题:

         然后我们通过交换可以看到,类的数据与s的数据交换了:

         这表明了什么?这表示我们将两次深拷贝变为了一次深拷贝加一次移动构造,这两者消耗的时间是一样的吗?不一样,这差距可太大了。

3.2 移动赋值

        // 移动赋值
        string& operator=(string&& s) noexcept
        {
            cout << "string& operator=(string&& s) -- 移动语义" << endl;
            swap(s);
            return *this;
        }

        对于移动赋值来说和移动构造差不了太多,博主也不演示了,没啥意思。对了,在STL库里的各种容器在C++11更新之后都支持了移动语义,如果有兴趣可以自行查看:

https://legacy.cplusplus.com/


        以上就是博主对于右值的全部理解了,希望能够帮助到大家。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值