【C++11 ——— 右值引用和移动语义】

右值引用和移动语义

左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值(Lvalue)
 左值是指可以取地址的表达式,通常是具名变量或对象。左值在赋值表达式中出现在等号的左边,表示一个持久的对象。左值可以通过取地址符(&) 获取其内存地址。

特征

  • 可以取地址。
  • 具有持久的生命周期,直到其作用域结束。
  • 例如:变量名、数组元素、解引用的指针等。

左值引用就是给左值的引用,给左值取别名。

左值引用(Lvalue Reference)
 左值引用是对左值的引用,使用符号 & 声明。左值引用可以绑定到左值,允许通过引用来访问和修改原始对象。

特征

  • 只能绑定到左值。
  • 可以通过左值引用修改原始对象的值。
  • 例如:int a = 10; int& ref = a;,ref是a的左值引用。

示例:

int main()
{
    // 以下的p、b、c、*p都是左值
    int* p = new int(0);   // p是指向动态分配的int对象的指针,是左值
    int b = 1;            // b是int变量,是左值
    const int c = 2;      // c是const int变量,是左值
    
    // 以下几个是对上面左值的左值引用
    int*& rp = p;         // rp是对p的左值引用,是左值
    int& rb = b;          // rb是对b的左值引用,是左值 
    const int& rc = c;    // rc是对c的const左值引用,是左值
    int& pvalue = *p;     // pvalue是对*p的左值引用,是左值

    // *p是对动态分配的int对象的解引用,是左值
    // new int(0)是动态分配int对象的右值表达式
    // 1和2是int字面值,是右值

    return 0;
}

右值(Rvalue)
 右值是指不能取地址的表达式,通常是临时对象,字面常量,表达式返回值,函数返回值。右值在赋值表达式中出现在等号的右边,表示不持久的值。C++11将右值细分为纯右值(prvalue)和将亡值(xvalue)。

右值的分类:

  • 纯右值(prvalue):
    表示临时对象不与任何对象关联的值
    例如:字面量(如1、true)、函数返回的非引用值(如int func() { return 42; })、表达式结果(如a + b)。
  • 将亡值(xvalue):
    表示即将被移动的对象,通常是一个临时对象的引用
    例如:通过std::move转换的对象、返回右值引用的函数。
#include <cmath> // 引入cmath库以使用fmin函数

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

    // 以下几个都是常见的右值
    10;                  // 10是一个字面值,属于右值
    x + y;              // x + y是一个表达式,其结果是一个右值
    fmin(x, y);         // fmin(x, y)的返回值是一个右值,表示x和y的最小值

    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;     // rr1是一个右值引用,绑定到右值10
    double&& rr2 = x + y; // rr2是一个右值引用,绑定到表达式x + y的结果
    double&& rr3 = fmin(x, y); // rr3是一个右值引用,绑定到fmin(x, y)的结果

    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    10 = 1;             // 10是右值,不能作为赋值的左操作数
    x + y = 1;         // x + y是右值,不能作为赋值的左操作数
    fmin(x, y) = 1;    // fmin(x, y)是右值,不能作为赋值的左操作数

    return 0;
}

需要注意的是,右值不能直接取地址。然而,当右值被绑定到一个右值引用时,它会被存储在特定的内存位置,这样就可以通过该引用获取这个位置的地址。

例如,虽然无法直接获取字面量10的地址,但在将其绑定到右值引用rr1后,可以获取rr1的地址,并且可以修改rr1的值。
如果不希望rr1的值被修改,可以使用const int&& rr1来引用,这样rr1将成为一个常量右值引用。

int main()
{
    double x = 1.1, y = 2.2; // 定义两个double类型的变量x和y

    int&& rr1 = 10;          // rr1是一个右值引用,绑定到右值10
    const double&& rr2 = x + y; // rr2是一个常量右值引用,绑定到表达式x + y的结果

    rr1 = 20;                // 将rr1的值修改为20,合法,因为rr1是非常量的右值引用

    rr2 = 5.5;               // 这里会报错:不能修改常量右值引用
    // 解释:rr2是一个const double&&,这意味着它绑定的值不能被修改。尝试给rr2赋值会导致编译错误。

    return 0; // 程序正常结束
}

左值引用与右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
#include <iostream>
using namespace std;

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10; // 定义一个整型变量a,并初始化为10

    // 创建一个左值引用ra1,引用变量a
    int& ra1 = a; // ra1是a的别名,可以通过ra1访问和修改a的值
    cout << "ra1: " << ra1 << endl; // 输出ra1的值,即10

    // int& ra2 = 10; // 编译失败,因为10是右值,左值引用不能绑定到右值

    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10; // ra3是一个常量左值引用,引用右值10
    const int& ra4 = a;  // ra4是一个常量左值引用,引用左值a

    // 输出ra3和ra4的值
    cout << "ra3: " << ra3 << endl; // 输出ra3的值,即10
    cout << "ra4: " << ra4 << endl; // 输出ra4的值,即10

    return 0; // 程序正常结束
}

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{
    // 右值引用只能绑定到右值,不能绑定到左值。
    int&& r1 = 10; // r1是一个右值引用,绑定到字面量10(右值)
    // 这行代码是合法的,因为10是右值。

    // 以下代码尝试将左值绑定到右值引用
    int a = 10;    // a是一个左值,初始化为10
    // int&& r2 = a; // 这行代码会导致编译错误
    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    // message : 无法将左值绑定到右值引用

    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a); // std::move将左值a转换为右值
    // r3现在是一个右值引用,绑定到a的右值版本

    return 0; // 程序正常结束
}

右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

右值引用肯定是要在某些场景比左值引用更加高效,所以才会引入右值引用的概念。
下面自己手动实现一个深拷贝的类,string类,观察其中的调用逻辑 :



namespace qq
{
	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[_size + 1];
			strcpy(_str, str);
			cout << "string(const char* str = "") -- 构造函数" << endl;
		}
		// 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;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		// 赋值重载
		//左值
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			char* tmp = new char[s._capacity + 1];
			strcpy(tmp, s._str);

			delete[] _str;
			_str = tmp;
			_size = s._size;
			_capacity = s._capacity;

			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)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str()const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};


	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		qq::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;
	}
}

左值引用的使用场景:

做参数和做返回值都可以提高效率:

  • 左值引用做参数,防止传参的时候进行拷贝操作。
  • 左值引用做返回值时,防止返回临时对象的时候进行拷贝构造。

void func1(qq::string s)
{}
void func2(const qq::string& s)
{}
int main()
{
	qq::string s1("hello world");
	cout << "------" << endl;

	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);
	cout << "------" << endl;
	func2(s1);
	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

在这里插入图片描述

  • 在上面的代码中,首先fun1fun2 分别接受string类型的参数,前者通过值传递,后者通过常量左值引用来传递。
  • 首先在构造对象s1的时候首先调用构造函数进行构造。
  • 其次在调用func(s1)的时候,会发生深拷贝,因为传入的是s1的一个副本,这就会发生深拷贝,因为传递的是一个副本。
  • 而在调用func(2)的时候,由于使用了常量左值引用,避免了拷贝,提高了效率。

左值引用的短板:

但是当函数的返回对象是一个局部变量的时候,当出了函数的作用域,该变量就被销毁了,因为其声明周期只限于函数作用域。所以此时就不能使用左值返回了,不得不只能继续使用传值返回,但是传值返回至少都有一次拷贝构造,这就造成了效率的低下。

在这里插入图片描述

左值引用中编译器的优化

在这里插入图片描述

  • 对于上面的str,其自身本是一个左值,传值返回,所以在销毁前会先创建一个临时对象,再用这个临时对象来赋值给s1
  • 所以理论上,在声明str的时候会调用一次构造函数
  • 其次在返回和赋值的时候会调用两次拷贝构造

在这里插入图片描述
但是实际上就只有一次拷贝构造,因为编译器会把连续的拷贝构造合为一个拷贝构造!
在这里插入图片描述

右值引用和移动语义

qq::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

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

我们添加了移动构造之后,继续执行上面的代码可以发现,此时直接调用了移动构造。
在这里插入图片描述

  • 这里to_string的返回值是一个临时对象,然而此时这个临时对象不才会去调用const 修饰的左值引用,而是直接调用了右值引用,此时直接窃取这个将亡值。在这里插入图片描述
    在这里插入图片描述

并且这里编译器也对其做了相对于的优化,直接一步移动构造到位!
在这里插入图片描述

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

在qq::string类中增加移动赋值函数,再去调用qq::to_string(123),不过这次是将
qq::to_string(123)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

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

在这里插入图片描述

  • 在这里,首先构造s1调用一次构造函数
  • 其次在to_string函数的内部再次调用构造函数来创建str
  • 在str返回的时候会调用移动构造创建出一个临时对象,最后再通过移动赋值向s1赋值。
  • 这里的移动构造和移动赋值只是完成了资源的交换,并没有拷贝,所以整体的效率得到了提高。

在这里插入图片描述

右值引用引用左值

根据 C++ 的语法规则,右值引用只能引用右值。那么,右值引用是否绝对不能引用左值呢?


实际上,在某些场景下,我们可能确实需要使用右值引用来引用左值,以实现移动语义。当我们需要将一个左值转化为右值引用时,可以使用 std::move 函数。 在 C++11 中,std::move() 函数位于 utility头文件中。
这个函数的名称可能会引起误解,因为它并不实际“搬移”任何东西。它的唯一功能是将一个左值强制转换为右值引用,从而实现移动语义。

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对左值的强转:

int main()
{
	qq::string s1("hello world");
	// 这里s1是左值,调用的是拷贝构造
	qq::string s2(s1);
	// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
	// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
	// 资源被转移给了s3,s1被置空了。
	qq::string s3(std::move(s1));
	return 0;
}

在这里插入图片描述

右值引用的其他使用场景

C++11标准出来之后,STL中的容器除了增加移动构造和移动赋值之外,STL容器插入接口函数也增加了右值引用版本。

以list容器的push_back接口为例:
在这里插入图片描述
在这里插入图片描述
当使用std的list时,因为其已经实现了右值引用,所以当我们传入需要深拷贝的自定义string时,其会调用string的构造函数,左值的就调用左值引用版本的,右值就调用右值引用版本的。

  • 第一个lt.push_back(qq::string("11111"))中,传入的是一个右值,故其直接调用移动构造即可完成插入。
  • 第二个qq::string s1("12345") lt.push_back(s1); 其中s1是一个左值,所以调用左值引用版本的push_back,故这是一次深拷贝
  • 第三个lt.push_back(move(s1));对于s1进行了move操作,强制其调用右值引用版本,即移动构造

完美转发

万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。比如

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

但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,具体见下面的示例:

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

我们一共实现了四种Fun函数,分别是左值引用,右值引用,const 左值引用 和 const 右值引用。
然后通过传入不同的左值右值以分别调用其函数,比如,传入右值10,就应该调用右值引用函数,但是实际上的结果是:
在这里插入图片描述
这也就应证了之前说的:右值引用属性本身是个左值、,也就是说右值引用后续使用中都退化成了左值

所以这里就需要用到完美转发了!

完美转发

模板中的&& 万能引用

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

template<class T>
void PerfectForward(T&& t)
{
	Func(std::forward<T>(t));
}

在使用完完美转发后,当PerfectForward函数传入的是右值时,就不会退化为左值,而是匹配到右值引用的Func函数中,传入左值时,类似。
在这里插入图片描述

完美转发的使用场景

下面实现了一个简化版本的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&& x)
	{
		//Insert(_head, x);
		Insert(_head, std::forward<T>(x));
	}
	void PushFront(T&& x)
	{
		//Insert(_head->_next, x);
		Insert(_head->_next, std::forward<T>(x));
	}
	void Insert(Node* pos, T&& x)
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<T>(x); // 关键位置
		// 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<qq::string> lt;
	lt.PushBack("1111");
	lt.PushFront("2222");
	return 0;
}

在这里插入图片描述

代码执行过程

main 函数中,调用 lt.PushBack("1111");lt.PushFront("2222"); 时,字符串字面量 "1111""2222" 被传递给 PushBackPushFront

第一次调用 PushBack("1111")

  1. 构造 std::string 对象

    • 字符串字面量 "1111" 被隐式转换为 std::string 对象,调用构造函数 string(const char* str)
    • 输出string(const char* str = ) -- 构造函数
  2. 调用 Insert 方法

    • Insert 中,std::forward<T>(x)x 作为右值传递给 newnode->_data
    • 由于 newnode->_datastd::string 类型,右值绑定到 newnode->_data 时,会调用移动构造函数(如果存在)。
    • 输出string(const char* str = ) -- 构造函数(用于创建 newnode_data
  3. 移动赋值

    • Insert 方法中,newnode->_data 的赋值可能会触发移动赋值操作(如果 std::string 的实现中有这样的逻辑)。
    • 输出string& operator=(string&& s) -- 移动赋值

第二次调用 PushFront("2222")

  1. 构造 std::string 对象

    • 字符串字面量 "2222" 被隐式转换为 std::string 对象,调用构造函数 string(const char* str)
    • 输出string(const char* str = ) -- 构造函数
  2. 调用 Insert 方法

    • 同样,std::forward<T>(x)x 作为右值传递给 newnode->_data
    • 由于 newnode->_datastd::string 类型,右值绑定到 newnode->_data 时,会调用移动构造函数(如果存在)。
    • 输出string(const char* str = ) -- 构造函数(用于创建 newnode_data
  3. 移动赋值

    • 移动赋值操作可能再次被触发。
    • 输出string& operator=(string&& s) -- 移动赋值

引用折叠

这里需要介绍一下引用折叠:
引用折叠是 C++ 中的一种规则,涉及到如何处理引用的组合。具体来说,当我们在模板中使用引用时,可能会出现引用的引用。`C++ 规定了引用的折叠规则:

左值引用 + 左值引用: 结果为左值引用(T& & 折叠为 T&)。
左值引用 + 右值引用: 结果为左值引用(T& && 折叠为 T&)。
右值引用 + 左值引用: 结果为左值引用(T&& & 折叠为 T&)。
右值引用 + 右值引用: 结果为右值引用(T&& && 折叠为 T&&)。

完美转发的机制是通过结合万能引用、引用折叠和 std::forward 来实现的。std::forward 的工作原理依赖于引用折叠的规则,以确保在转发参数时能够正确处理左值和右值。

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++11中,右值引用的作用之一是实现移动语义,即对象的资源所有权的转移。在C++11之前,移动语义的缺失是C++所面临的一个问题。右值引用也可以看作是一块空间的别名,只能引用右值。通过使用右值引用,我们可以对右值进行引用,并且可以实现对移动语义的支持。右值引用的语法是在类型后面加上两个&&。在函数返回值为临时变量的情况下,可以使用右值引用来接收该临时变量。另外,右值引用还可以引用经过move操作后的左值,通过使用move函数,可以改变左值的属性,使其变成右值。总之,右值引用C++中的作用主要是支持移动语义,提高程序的性能和效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [c++右值引用具体用法](https://download.csdn.net/download/weixin_38734492/14887141)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [C++11——右值引用](https://blog.csdn.net/weixin_57023347/article/details/120957689)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值