C++11【右值引用详解】

🏞️1. 左值引用和右值引用

在之前,我们学习过左值引用,在C++11中,新增了一个语法特性:右值引用但无论左值引用还是右值引用,都是给对象取别名.

那么首先,什么是左值,什么是左值引用

左值是一个表示数据的表达式,我们可以获取它的地址+可以对它赋值,左值引用可以出现在赋值符号的左边,右值不能出现在赋值符号左边,定义时,const修饰的左值不能赋值,但是可以取地址,左值引用就是给左值取别名.

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

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

int main()
{
    double x = 1.1, y = 2.2;

    //以下几个都是常见的右值
    10;
    x + y;
    min(x, y);  //传值返回

    //以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = min(x, y);

    return 0;
}

注意:右值是不能取地址的,但是给右值取别名后,会导致右值被存储在特定的位置,且可以取到该位置的地址,例如:不能对字面量10取地址,但是被rr1引用后,可以对rr1取地址,也可以修改rr1.

🌁2. 左值引用与右值引用的比较

左值引用总结:

  1. 左值引用只能引用左值
  2. 但是const左值引用既能引用左值,也能引用右值.
int main()
{
    //int& r = 10;  错误!左值引用不能引用右值

    const int& r = 10; //const 左值引用可以引用右值

    return 0;
}

右值引用总结:

  1. 右值引用只能引用右值,不能引用左值
  2. 但是右值引用可以引用move以后的左值
int main()
{
    int a = 10;

    //int&& ra = a;  错误! 右值引用只能引用右值

    int&& ra = std::move(a);  //右值引用可以引用move以后的左值

    return 0;
}

🌠3. 右值引用的使用场景

刚才我们提到,左值引用既可以引用左值又可以引用右值(使用const 左值引用),那为什么C++11还要提出右值引用呢?在一些,场景下,左值引用具有短板:

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 += '-';
    }

    std::reverse(str.begin(), str.end());
    return str;
}

这样的一个to_string函数,最终返回值是一个局部变量,无法用左值引用返回,此时,我们就需要右值引用来完成.

对于这个to_string函数,我们进行一次调用:

//这是string的拷贝构造函数
string(const string& s)
    :_str(nullptr)
        , _size(0)
        , _capacity(0)
{
    cout << "string(const string& s) -- 深拷贝" << endl;

    string tmp(s._str);
    swap(tmp);
}


int main()
{
    MyString::string str = MyString::to_string(1121);

    return 0;
}

运行结果如下:

image-20221029203147682

可以看到,它调用了一次拷贝构造:

image-20221029204440326

image-20221029204134349

用右值引用和移动语义来解决上述问题:

MyString中增加移动构造,移动构造的本质是将参数右值的资源窃取过来,占为己有,那就不需要去深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己.

string(string&& s)
    :_str(nullptr)
    , _size(0)
    , _capacity(0)
{
    cout << "string(string&& s) -- 资源转移" << endl;
    swap(s);
}

image-20221029210352829

int main()
{
	MyString::string str = MyString::to_string(1121);

	return 0;
}

在增加了移动构造后,我们再来运行这句代码:

image-20221029210911900

过程分析:

image-20221029211122418

但是为什么运行结果显示只有一次移动构造呢?

image-20221029211600997

所以这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造没有新开辟空间,拷贝数据,效率提高.

不仅仅有移动构造,还有移动赋值:

MyString中增加移动赋值函数,再去调用to_string(1121),不过这次是将to_string(1121)返回的右值对象赋值给str,这时调用移动赋值.

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

int main()
{
    MyString::string str;
    str = MyString::to_string(1234);
    return 0;
}

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象作为to_string函数调用的返回值赋值给str,这里调用的移动赋值

🌌4. 右值引用引用左值

按照语法,右值引用只能引用右值,但右值引用就一定不能引用左值吗?

在有些场景下,可能需要右值引用去引用左值实现移动语义,当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值.

C++11中,std::move()函数位于头文件<utility>中,该函数名字有些迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值,然后实现移动语义.

比如,在C++11中,STL中的容器插入类的接口函数也实现了右值引用版本:

image-20221030234300580

为什么要去实现呢?

我们来看下面的代码:

我们创建了一个存储string对象的list容器,并且分别用左值(s1),和右值("3333","std::move(s1)")向其中插入元素,来看看运行结果:

int main()
{
	list<MyString::string> lt;
	MyString::string s1("1111");

	//深拷贝
	lt.push_back(s1);

	//移动构造(资源转移)
	lt.push_back("3333");
	lt.push_back(std::move(s1));

	return 0; 
}

image-20221030235221652

可以看到,运行结果和我们在代码注释中所描述的一样,那么,为什么是这样的结果呢?

image-20221031000313446

但是,在std::move(s1)并插入s1后,s1的资源会被转移到newnode上面,这时s1便不能再使用,所以,在使用std::move()函数时需要谨慎操作.

对于lt.push_back("3333"),可以避免拷贝构造,提高效率.

⛺5. 完美转发

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;
}

运行结果:

image-20221031002435875

对于上述代码中描述的万能引用,它能同时接收左值和右值,但是却无法保持它们的属性,这样就会造成问题:

template<typename T>
void PerfectForward(T&& t)
{
	MyString::string copy = t;
}

int main()
{
	MyString::string s1("2222");

	PerfectForward(MyString::string("1111"));

	return 0;
}

运行结果:

image-20221031005944204

在这里,我们传入了一个临时对象(右值),我们期望通过它的右值属性从而在构造string时能够进行移动构造的调用,但在这里,由于万能引用不能保持它的属性,对于右值引用,它本身是一个左值,所以这里调用了深拷贝.

如果要解决这种问题,需要使用std::forward<T>(t)

template<typename T>
void PerfectForward(T&& t)
{
	MyString::string copy = std::forward<T>(t);;
}

int main()
{
	MyString::string s1("2222");

	PerfectForward(MyString::string("1111"));

	return 0;
}

运行结果:

image-20221031010426578

📖6. 完美转发的应用场景

//简易版的list
template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};

template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void PushBack(T&& xx)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(xx)); //使用完美转发保持右值属性
	}

	void PushBack(const T& x)
	{
		//Insert(_head, x);
		Insert(_head, x);
	}

	void PushFront(T&& xx)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(xx)); //使用完美转发保持右值属性
	}

	void Insert(Node* pos, T&& xx)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(xx); // 关键位置,使用完美转发保持右值属性

		// prev newnode pos

		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}

	void Insert(Node* pos, const T& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = x;

		// prev newnode pos

		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}
private:
	Node* _head;
};

int main()
{
	List<MyString::string> lt;

	MyString::string s1("11111");

	lt.PushBack(s1);
	lt.PushBack("2222");

	return 0;
}

运行结果:

image-20221031013311959

image-20221031013243460

可以看到,对于PushBack操作,如果插入右值,我们使用了完美转发来在值传递过程中保持它的右值属性,从而保证最终调用节点中所存类型值的移动构造,提升效率.

文章中用到的MyString代码:

namespace MyString
{
	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(string&& s)
		 	:_str(nullptr)
		 	, _size(0)
		 	, _capacity(0)
		 {
		 	cout << "string(string&& s) -- 资源转移" << endl;
		 	swap(s);
		 }

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		 //移动赋值
		 string& operator=(string&& s)
		 {
		 	cout << "string& operator=(string&& s) -- 资源转移" << endl;
		 	swap(s);

		 	return *this;
		 }

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			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
	};


	MyString::string operator+(const MyString::string& s, char ch)
	{
		MyString::string ret(s);
		ret += ch;

		return ret;
	}

	MyString::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		MyString::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}
}
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉默.@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值