C++11--右值引用与移动语义

文章详细介绍了C++中的左值和右值的概念,以及在C++11中引入的右值引用,包括它们的特点、使用场景和意义。右值引用主要用于解决左值引用在某些场景下无法避免拷贝构造的问题,通过移动语义(移动构造和移动赋值)提高了效率。此外,文章还提到了完美转发和万能引用的概念,以及它们在函数参数传递中的应用。
摘要由CSDN通过智能技术生成

目录

基本概念

左值与右值

左值引用与右值引用

右值引用的使用场景和意义

左值引用的使用场景

右值引用和移动语义

移动构造和拷贝构造的区别

编译器的优化

移动赋值和赋值运算符重载的区别

右值引用的其他应用场景

完美转发

万能引用

完美转发保持值属性

完美转发的使用场景


基本概念

左值与右值

什么是左值?

左值是一个表示数据的表达式,如:变量名或解引用的指针
它的两个特点
·我们可以获取它的地址也可以对它赋值(const修饰除外)

·左值既可以出现在表达式的左边也可以出现在表达式的右边

//可以取地址对象,就是左值
int main()
{
	int a = 10;
	int& r1 = a;
	int* p = &a;
	int& r2 = *p;

	const int b = 10;
	const int& r3 = b;

	return 0;
}

什么是右值?

  右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等 
它的两个特点

·右值不能被取地址也不能被修改

·右值只能出现在表达式的右边不能出现在表达式左边

//不能取地址对象,就是右值
int main()
{
	double x = 1.1, y = 2.2;

	//常见的右值
	10;
	x + y;
	fmin(x, y);

	//cout << &fmin(x, y) << endl;

	return 0;
}

左值引用与右值引用

传统的C++语法中就有引用的语法,而在C++11中更新了右值引用的语法。为了进行区分,我们将C++11之前的引用叫做左值引用,将C++11之后更新的引用叫做右值引用,不论是左值引用还是右值引用,它们的本质都是 “取别名”。

左值引用

左值引用就是对于左值的引用,即对左值取别名,通过&来声明

下面是一段代码示例

// 以下的p b c *p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pv= *p;

右值引用

右值引用就是对右值的引用,即对右值取别名,通过&&来声明

下面是一段代码示例

//不能取地址对象,就是右值
int main()
{
	double x = 1.1, y = 2.2;

	//常见的右值
	10;
	x + y;
	fmin(x, y);

	//cout << &fmin(x, y) << endl;

	//右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);

	return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1.
如果不想rr1被修改,可以用const int&& rr1 去引用.
左值引用可以引用右值吗?
  • 左值引用不能引用右值,左值是可以被修改的而右值是不可以被修改的,这里涉及到一个权限放大的问题
  • 如果想要用左值引用来引用右值,需要用到const关键字来修饰左值引用,因为经过const修饰后左值引用就没有修改的权限了

因此const左值引用可以引用左值也可以引用右值

template<class T>
void func(const T& val)
{
	cout << val << endl;
}

int main()
{
	string s("hello");
	func(s);                    //s为变量 左值

	func("world");              // "world"是常量 右值
	return 0;
}

右值引用可以引用左值吗?

  • 右值引用只能引用右值不能引用左值
  • 如果想要用右值引用来引用左值,需要用到move函数

move函数是C++11标准提供的一个函数,被move后的左值能够被右值引用引用

int main()
{
	//左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	//左值引用能否引用右值 -- 不能直接引用,但是const左值引用可以引用右值
	//void push_back(const T& x)
	const int& r1 = 10;
	const double& r2 = x + y;
	const double& r3 = fmin(x, y);

	//右值引用能否引用左值 -- 不能直接引用,但是右值引用可以引用move以后左值
	int*&& rr1 = move(p);
	int&& rr2 = move(*p);
	int&& rr3 = move(b);
	const int&& rr4 = move(c);

	return 0;
}

右值引用的使用场景和意义

虽然使用const修饰的左值引用能够同时引用左值和右值,但是左值引用终究是存在一些缺陷,而C++11提出的右值引用正是用来解决这些缺陷的

我们写出一个简单string类来

namespace qwe
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_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)
			, _size(0)
			, _capacity(0)
		{
			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()
		{
			delete[] _str;
			_str = nullptr;
		}

		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 tmp(*this);
			push_back(ch);

			return tmp;
		}

		const char* c_str() const
		{
			return _str;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;   
	};
}

左值引用的使用场景

在说明左值引用的缺陷之前我们先来看它的使用场景

  • 做参数--防止传参时进行拷贝构造
  • 做返回值--防止返回时对返回对象进行拷贝构造
void func1(shy::string s)
{}

void func2(const shy::string& s)
{}

int main()
{
	shy::string s("hello world");
	func1(s);                       
	func2(s);                        // 左值引用传参
	s += 'X';                        // 左值引用返回
	return 0; 
}

func1它传递的参数是形式参数,他是实际参数的一份临时拷贝

func2它传递的参数是s的别名,是左值引用

最后是+=,它返回的也是一份左值引用

string的拷贝是深拷贝,深拷贝的代价是很高的,所以说这里的左值引用效果很明显

左值引用的缺陷
左值引用虽然能避免不必要的拷贝操作 但是缺不能完全避免

左值引用做参数,能够完全避免传参时的拷贝操作
左值引用做返回值,不能完全避免函数对象返回时的拷贝操作
如果函数返回对象是一个局部变量,那么该变量出了局部作用域就会被销毁

这种情况下不能使用左值引用作为返回值,只能传值返回,这就是左值引用的短板

比如说我们实现一个to_string函数 将字符串转化为int类型 此时它的返回值就必须要是值拷贝 如果使用左值引用返回就会返回一个销毁的局部变量

代码表示如下

namespace qwe 
{
	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 += (x + '0');
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}

我们在调用to_string函数返回的时候会调用拷贝构造函数

C++11提出右值引用就是为了解决左值引用的这个缺陷,但是它的解决方法并不是单纯的将右值引用作为返回值

右值引用和移动语义

右值引用和移动语义解决上述问题的方式就是增加移动构造和移动赋值

移动构造

移动构造是一个构造函数 它的参数是右值引用类型

移动构造的本质就是将传入右值的资源转移过来 

代码表示如下

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

移动构造和拷贝构造的区别:

在没有增加移动构造之前 由于拷贝构造使用的是const左值引用来接受参数 因此无论是左值还是右值 都会调用拷贝构造函数
增加移动构造之后 由于移动构造采用的是右值引用来接受参数 因此如果拷贝构造对象时传入的是右值 那么就会调用移动构造
 拷贝构造进行的是深拷贝 而移动构造只需要调用swap函数进行资源转移即可 因此移动构造的代价比拷贝构造的代价小很多
给string类增加移动构造之后 对于返回局部string类对象的函数 返回string类对象的时候会调用移动构造进行资源的转移 不会像原来一样进行深拷贝了

演示效果如下

对于to_string当中返回局部的string对象是一个左值 一个临时变量 由于它出了局部作用域就会被销毁 被消耗的值我们将它叫做 “将亡值” 匿名对象也可以被称为 “将亡值”,因此对待这种 “将亡值” 编译器会将它识别为右值 这样就可以匹配搭配参数为右值的移动构造函数

编译器的优化

当一个函数在返回局部对象时,会先用局部对象拷贝出一个临时对象,然后再用这个临时拷贝的对象来拷贝定义的对象

 对于深拷贝的类会进行两次深拷贝 但是大部分编译器为了提高效率都对这种情况进行了优化 优化成了一次深拷贝

效果图如下

如果不进行优化 这里应该会调用拷贝构造和移动构造
如果进行了优化 这里就只会进行一次移动构造了
但是我们如果不是用函数的返回值来构造出一个对象 而是用一个之前已经定义过的对象来接受函数的返回值 这里就无法进行优化了

示例图如下

        对于返回局部对象的函数 就算只是调用函数而不接收该函数的返回值 也会存在一次拷贝构造或移动构造 因为函数的返回值不管接不接收都必须要有 而当函数结束后该函数内的局部对象都会被销毁 所以就算不接收函数的返回值也会调用一次拷贝构造或移动构造生成临时对象

移动赋值

移动赋值是对于赋值运算符重载的一个重载函数 该函数的参数是右值引用类型

在当前的string类中增加一个移动赋值函数 就是调用swap函数将传入右值的资源窃取过来

代码表示如下

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

移动赋值和赋值运算符重载的区别

在没有增加移动赋值之前 赋值运算符重载是使用const左值引用来接受参数 无论传入的是左值还是右值 都会调用它
增加移动赋值之后 由于移动赋值采用的是右值引用来接受参数 因此如果移动赋值传入的是右值 那么就会调用移动赋值
原本赋值时是调用拷贝构造进行了深拷贝 而移动赋值只需要调用swap函数进行资源转移即可 因此移动赋值的代价比赋值运算符重载小的很多

STL中的容器

以string为例

移动构造

移动赋值

move函数

move函数它并不能移动过任何值 它的功能是将一个左值强制转化为右值引用 然后实现移动语义

move函数的定义如下

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
	//forward _Arg as movable
	return ((typename remove_reference<_Ty>::type&&)_Arg);
}
  • move函数模板中_Arg参数的类型不是右值引用而是万能引用 万能引用和右值引用的形式一样 但是右值引用是需要确定的类型
  • 一个左值被move之后它的资源有可能被转移给别的数据了 所以说慎用被move后的左值

右值引用的其他应用场景

插入函数

 如果list中插入的对象类型是string

list<qwe::string> ls;
qwe::string s("1111");

ls.push_back(s);                   // 拷贝构造

ls.push_back("2222");              // 移动构造
ls.push_back(qwe::string("3333")); // 移动构造
ls.push_back(std::move(s));        // 移动构造
效果如下

完美转发

万能引用

模板中的&&不代表右值引用 而是万能引用 这样它既能接收左值又能接收右值 

template<class T>
void PerfectForward(T&& t)
{
	
}

右值引用和万能引用的区别就是 右值引用需要确定类型 而万能引用会根据传入的类型进行推导 如果传入的实参是一个左值 那么这里的形参t就是左值引用 如果传入的实参是一个右值 那么这里的形参t就是右值引用

下面重载了四个func函数 这四个func函数的参数分别左值引用 const左值引用 右值引用和const右值引用

我们在主函数中使用完美引用模板函数来调用func函数

代码表示如下
 

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

void PerfectForward(int&& t)
{
	Fun(t);
}

void PerfectForward(const int& 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;
}

不管传入何种类型 最后调用的都是左值引用而不是右值引用

因为只要右值经过一次引用之后右值引用就会被储存到特定位置 这个右值就可以被取地址和改 所以在经过一次参数传递之后右值就会退化为左值 如果我们想要让他保持右值的属性 这个时候就要用到完美转发

完美转发保持值属性

要想在参数传递过程中保持其原有的属性 需要在传参时调用forward函数

代码表示如下

//模板中的&& 表示万能引用,既能接收左值又能接收右值
//会退化成左值   --  完美转发  
template<typename T>
void PerFectForeard(T&& t)
{
	Fun(std::forward<T>(t));
}

完美转发的使用场景

下面提供一个简单的list类 分别提供了左值引用和右值引用的接口函数

	template<class T>
	struct ListNode
	{
		T _data;
		ListNode* _next = nullptr;
		ListNode* _prev = nullptr;
	};
	template<class T>
	class list
	{
		typedef ListNode<T> node;
	public:
		//构造函数
		list()
		{
			_head = new node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//左值引用版本的push_back
		void push_back(const T& x)
		{
			insert(_head, x);
		}
		//右值引用版本的push_back
		void push_back(T&& x)
		{
			insert(_head, std::forward<T>(x)); //完美转发
		}
		//左值引用版本的insert
		void insert(node* pos, const T& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = x;

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
		//右值引用版本的insert
		void insert(node* pos, T&& x)
		{
			node* prev = pos->_prev;
			node* newnode = new node;
			newnode->_data = std::forward<T>(x); //完美转发

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		node* _head; //指向链表头结点的指针
	};

定义一个list对象 储存我们之前实现的list类 我们分别传入左值和右值调用不同版本的push_back函数

	qwe::list<qwe::string> lt;
	qwe::string s("1111");
	lt.push_back(s);           //调用左值引用版本的push_back

	lt.push_back("2222");      //调用右值引用版本的push_back

我们在实现push_back的时候复用了insert的代码 对于左值引用的insert函数来说 它会先new一个节点 然后将对应的左值赋值给这个节点 调用赋值运算符重载 又因为赋值运算符重载本质上复用了拷贝构造 
对于右值版本的push_back函数 它复用了insert的代码 对于右值引用的insert函数来说 它会先new一个节点 然后将对应的右值赋值给这个节点 调用移动构造来进行转移资源
这其中调用函数传参的时候多处用到了 完美转发 这是因为如果不使用完美转发就会让右值退化为左值 最终导致多一次深拷贝 从而降低效率
 
如果我们想要保持右值的属性 每次传参的时候就必须要使用完美转发

与STL中的list的区别

如果将刚才测试代码中的list换成STL当中的list

调用左值版本的push_back插入节点时 在构造结点时会调用string的拷贝构造函数
调用右值版本的push_back插入节点时 在构造结点时会调用string的移动构造函数
而我们实现的list代码却使用的是赋值运算符重载和移动赋值

这是因为我们是使用的new操作符来申请空间 new操作符申请空间之后会自动调用构造函数进行初始化

而初始化之后就只能使用赋值运算符重载了

而STL库中使用空间配置器获取内存 因此在申请到内存后不会调用构造函数对其进行初始化 是后续用左值或右值对其进行拷贝构造 所以会产生这样子的结果

如果我们想要达到STL中的效果 我们只需要使用malloc开辟空间 然后使用定位new进行初始化就可以了

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值