IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 左值,右值,std::move,移动构造,移动赋值等 -> 正文阅读

[C++知识库]左值,右值,std::move,移动构造,移动赋值等

左值和右值

代码例子根据这篇博客修改而来
move的描述引用这篇博客
左值是一般是出现在等号左边的,是可以取地址的。右值一般出现在等号右边,不可以取地址。非 const 的变量都是左值。函数调用的返回值若不是引用,则该函数调用就是右值。一般的“引用”都是引用变量的,而变量是左值,因此它们都是“左值引用”。
C++11 新增了一种引用,可以引用右值,因而称为“右值引用”。无名的临时变量不能出现在赋值号左边,因而是右值。右值引用就可以引用无名的临时变量。定义右值引用的格式如下:

类型 && 引用名 = 右值表达式;
例如

#include <iostream>
using namespace std;
 
int main()
{
	int num = 10;
	//int && a = num;  //右值引用不能初始化为左值
	int && a = 10;
	a = 100;
	cout << a << endl;//输出100
 
	system("pause");
	return 0;
}

std::move

先看一个使用的例子

int &&rr1 = 42; //正确:字面常量的右值
int &&rr2 = rr1; //错误:表达式rr1是左值

为了能避免此类错误,标准库引入std::move()

int &&rr2 = std::move(rr1); //正确

因为move的作用就是强制类型转换,将传入的左值rr1强转为右值,除此之外没有做任何事。

例子

#include <string>
#include <vector>
#include <iostream>
#include <variant>
using namespace std;

class A {
public:
	int x;
	A(int x) : x(x)
	{
		cout << "Constructor" << endl;
	}
	A(A& a) : x(a.x)
	{
		cout << "Copy Constructor" << endl;
	}
	A& operator=(A& a)
	{
		x = a.x;
		cout << "Copy Assignment operator" << endl;
		return *this;
	}
	A(A&& a) : x(a.x)
	{
		//这里没有做a.x = 0;操作,不是很规范,具体看下一段代码
		cout << "Move Constructor" << endl;
	}
	A& operator=(A&& a)
	{
		x = a.x;
		cout << "Move Assignment operator" << endl;
		return *this;
	}
};

int main()
{
	A a(1);//调用构造
	A e(2);//调用构造
	e = A(a);//先调用拷贝构造 在调用移动赋值运算符 
	A d = move(e);//这等价于A d(move(e)); 调用Move Constructor 
	system("pause");
	return 0;
}

输出为:

Constructor
Constructor
Copy Constructor
Move Assignment operator
Move Constructor

首先要清楚为什么要自定义移动构造(右值构造)和移动赋值(右值赋值)这两个函数?
这个问题在文章最后回答。
这里面移动构造对应以前学过的拷贝构造函数,但是移动构造传入的是右值,即这个函数的参数类型是A&& a,而不是拷贝构造函数的A& a。
这里面移动赋值对应以前学过的赋值运算符Copy Assignment operator,它们最大的不同也是传入的参数不同,移动赋值是A&& a,赋值运算是A& a。
上面这段代码首先用构造函数调用a和e两个对象。在e = A(a)中,首先 A(a)会调拷贝构造Copy Constructor,然后由于e是存在的对象,则调用移动赋值。
对于A d = move(e) 其实它就是A d(move(e)) ,move(e)将e变为右值,然后在对新对象d构造时,发现传入的参数是A && 即右值,便会调用移动构造,而不是普通的构造函数。

再看一个例子

#include <string>
#include <vector>
#include <iostream>
#include <variant>
using namespace std;

class A {
public:
	int x;
	A(int x) : x(x)
	{
		cout << "Constructor" << endl;
	}
	A(A& a) : x(a.x)
	{
		cout << "Copy Constructor" << endl;
	}
	A& operator=(A& a)
	{
		x = a.x;
		cout << "Copy Assignment operator" << endl;
		return *this;
	}
	A(A&& a) : x(a.x)
	{
		a.x = 0;
		cout << "Move Constructor" << endl;
	}
	A& operator=(A&& a)
	{
		x = a.x;
		cout << "Move Assignment operator" << endl;
		return *this;
	}
};

int main()
{
	A* a = new A(1);
	A* b = new A(1);
	A* c = new A(1);

	shared_ptr<A> p1(a);
	shared_ptr<A> p2(b);
	shared_ptr<A> p3(c);


	
	cout << "before move " << p1.get() << endl;
	A res1 = std::move(*p1);
	cout << "after move " << p1.get() << endl;

	
	cout << "before move " << p2.get() << endl;
	shared_ptr<A>&& res2 = std::move(p2);
	cout << "after move " << p2.get() << endl;

	cout << "before move " << p3.get() << endl;
	shared_ptr<A> res3= std::move(p3);
	cout << "after move " << p3.get() << endl;
	
	system("pause");
	return 0;

}

输出结果

Constructor
Constructor
Constructor
before move 009ED6B0
Move Constructor
after move 009ED6B0
before move 009ED7C8
after move 009ED7C8
before move 009ED7F8
after move 00000000

在A res1 = std::move(p1);执行后,会调用移动构造Move Constructor,因为p1实际就是对象a,这里把对象从左值转到了右值,于是调用了移动构造,跟上一个例子的A d = move(e);是一样的
但是由于我们对Move Constructor将进行了小小的修改,将等号右边的a的x设为0,所以执行后a
的x为0。即修改成这样后才符合移动的语义,毕竟移动嘛,就应该直接把右边的值“掏空”转移到等号左边。
所以以后构建一个新类的时候,如果需要创建这移动构造或者移动赋值都需要将类的基本类型(如果是int就设为0),指针置空,复杂类型继续调用复杂类型的移动构造或者移动赋值。保证移动之后,该对象什么都没有了,并且移动的过程最好是直接进行指针的转移,这样能保证移动的高效性,而不会由于资源的拷贝而导致多余的计算操作。

shared_ptr&& res2 = std::move(p2);执行后,res2本身是一个右值,当你赋值的时候,res2本身就是p2的别名。

shared_ptr res3= std::move(p3);执行后,res3以右值构造函数接收参数,所以stl根据右值规则,拿走了p3的值,并且制空p3. 这个置空 是因为这个智能指针类在右值构造函数自己实现了资源的转移操作 所以才置空的。

具体如果想看具体实现移动构造和移动赋值的类,可以参考开头的超链接里的,这里我直接粘贴到下面,当然更建议的是看官方类的实现方法,比如智能指针类。

#include <iostream>
#include <string>
#include <cstring>
using namespace std;
 
class String
{
public:
	char* str;
	String() : str(new char[1])
	{
		str[0] = 0;
	}
 
	// 构造函数
	String(const char* s)
	{
		cout << "调用构造函数" << endl;
		int len = strlen(s) + 1;
		str = new char[len];
		strcpy_s(str, len, s);
	}
 
	// 复制构造函数
	String(const String & s)
	{
		cout << "调用复制构造函数" << endl;
		int len = strlen(s.str) + 1;
		str = new char[len];
		strcpy_s(str, len, s.str);
	}
 
	// 复制赋值运算符
	String & operator = (const String & s)
	{
		cout << "调用复制赋值运算符" << endl;
		if (str != s.str)
		{
			delete[] str;
			int len = strlen(s.str) + 1;
			str = new char[len];
			strcpy_s(str, len, s.str);
		}
		return *this;
	}
 
	// 移动构造函数
	// 和复制构造函数的区别在于,其参数是右值引用
	String(String && s) : str(s.str)
	{
		cout << "调用移动构造函数" << endl;
		s.str = new char[1];
		s.str[0] = 0;
	}
 
	// 移动赋值运算符
	// 和复制赋值运算符的区别在于,其参数是右值引用
	String & operator = (String && s)
	{
		cout << "调用移动赋值运算符" << endl;
		if (str != s.str)
		{
			// 在移动赋值运算符函数中没有执行深复制操作,
			// 而是直接将对象的 str 指向了参数 s 的成员变量 str 指向的地方,
			// 然后修改 s.str 让它指向别处,以免 s.str 原来指向的空间被释放两次。
			str = s.str;
			s.str = new char[1];
			s.str[0] = 0;
		}
		return *this;
	}
 
	// 析构函数
	~String()
	{
		delete[] str;
	}
};
 
template <class T>
void MoveSwap(T & a, T & b)
{
	T tmp = move(a);  //std::move(a) 为右值,这里会调用移动构造函数
	a = move(b);  //move(b) 为右值,因此这里会调用移动赋值运算符
	b = move(tmp);  //move(tmp) 为右值,因此这里会调用移动赋值运算符
}
 
template <class T>
void Swap(T & a, T & b) 
{
	T tmp = a;  //调用复制构造函数
	a = b;  //调用复制赋值运算符
	b = tmp;  //调用复制赋值运算符
}
 
int main()
{
	String s;
	// 如果没有定义移动赋值运算符,则会导致复制赋值运算符被调用,引发深复制操作。
	s = String("this");  //调用移动赋值运算符
	cout << "print " << s.str << endl;
	String s1 = "hello", s2 = "world";
	//MoveSwap(s1, s2);  //调用一次移动构造函数和两次移动赋值运算符
	Swap(s1, s2);//调用一次复制构造函数,两次复制赋值运算符
	cout << "print " << s2.str << endl;
 
	system("pause");
	return 0;
}

回答上面的那个问题:
所以这两个移动函数的目的是,将右值的所有资源直接转移到左边,而不是拷贝和赋值中,对对象等资源的深度复制,这些复制都是需要花费计算资源和时间的。因为我们已经用move将左值转换成了右值,根本不需要关心右值以后还需要用到,因为右值的定义就是一个临时对象。所以你可以在类实现这两个移动函数的时候,直接粗暴的将资源全部转移到左值中,对于类中的资源最好不要拷贝,直接进行指针所有权的转移。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-26 11:24:29  更:2022-04-26 11:28:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 22:45:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码