C++基础知识,对象移动,拷贝构造函数,移动拷贝构造函数,赋值运算符,移动赋值运算符

如果没耐心看详细的讲解,直接到最下面看一个统一的例子

使用到的类:

class classB
{
public:
	int val1;
public:
	classB() :val1(100) 
	{
		cout << "B类构造函数执行了" << endl;
	};

	//拷贝构造函数
	classB(const classB& obj) :val1(obj.val1) 
	{
		cout << "B类拷贝构造函数执行了" << endl;
	};
	//析构函数
	virtual ~classB()
	{
		cout << "B类析构函数执行了" << endl;
	}

	//移动构造函数
	classB(classB&& obj)
	{
		cout << "B类!!!移动!!!构造函数执行" << endl;
	}
};


class classA
{
private:
	classB *innerClassB;

public:
	classA() :innerClassB(new classB())
	{
		cout << "A类构造函数执行" << endl;
	}

	//拷贝构造函数
	classA(const classA& obj):innerClassB( new classB( *(obj.innerClassB) ) )
	{
		cout << "A类拷贝构造函数执行" << endl;
	}

	virtual ~classA()
	{
		delete innerClassB;
		cout << "A类析构函数执行" << endl;
	}


	//移动构造函数(右值)  接管原来的内存
	//noexcept:通知标准库,函数不抛出任何异样,提升编译器工作效率
	classA(classA&& obj) noexcept:innerClassB(obj.innerClassB)
	{
		//打断原来的指针,切记
		obj.innerClassB = nullptr;
		cout << "A类!!!移动!!!构造函数执行" << endl;
	}


	//拷贝赋值运算符
	classA& operator=(const classA& obj)
	{
		if (this == &obj)
		{
			return *this;
		}
		delete innerClassB;
		//可不能这样写,这是移动赋值运算符的写法,拷贝可不销毁原先的
		//innerClassB = obj.innerClassB;
		cout << "执行了类A的拷贝赋值运算符" << endl;
		return *this;
	}

	//移动赋值运算符,因为凡是带有“移动”概念的,都要部分摧毁传进来的对象,所以参数肯定不会带const
	//并且都是右值,需要用std::move()传递进来
	classA& operator=(classA&& obj)
	{
		if (this == &obj)
		{
			return *this;
		}
		//斩断自己
		delete innerClassB;
		//重新指向
		innerClassB = obj.innerClassB;
		//斩断参数
		obj.innerClassB = nullptr;
		cout << "执行了类A的!!!移动赋值!!!运算符" << endl;
	}
};

对象移动

对象拷贝,消耗性能,提出对象移动的概念
许多临时对象生命周期短,把临时对象中的某些所有权拿过来,知销毁不需要的数据即可
把一个不想用的对象中有用的数据供自己使用。
说白了,就是因为我们平时写的代码,会有大量的临时对象产生,你的对象小了还好,要是一个vector,里面装了1000个巨大的对象,对象里面又有复杂的数据,那么这样的东西让系统产生许多临时对象就吃不消了,所以产生了对象移动的概念,就是在产生新的对象时,让就对象的一部分数据的所有权交给我们的新对象,从而旧对象销毁,而新的对象也不必申请可复用的一部分的数据空间,这是既节省了时间,又节省了空间,谁说空间和时间一定是杠杆的?

那什么叫移动呢?
移动:

  • A移动到B,那么A就不能再使用了
  • 并不是内存变更,而是内存中数据的所有权变更,内存是不变的

C++11:移动构造函数,拷贝构造函数:进一步提高程序效率

  • 拷贝构造函数:classA::classA(const classA& obj){…}左值引用
  • 移动构造函数:classA::classA(const classA&& obj){…}右值引用

如果有其他参数的话,必须要有默认值,和拷贝函数一样

移动构造函数

应该完成的事情

  • 1、完成必要的内存移动,斩断原对象的关系
  • 2、确保移动后源对象处于一种“即使被销毁也没有什么问题”的状态,既然移动了,就应该使用移动后的对象
    我们先看一下移动构造函数的写法
	//移动构造函数,这里是右值引用
	classB(classB&& obj)
	{
		//在这里完全可以进行内存所有权的交接
		//1、删除现在对象的部分内存所有权、
		//2、拿到传过来的对象的部分内存所有权
		//3、斩断传递对象的部分内存所有权
		cout << "B类!!!移动!!!构造函数执行" << endl;
	}
	//移动构造函数(右值)  接管原来的内存
	//noexcept:通知标准库,函数不抛出任何异样,提升编译器工作效率
	classA(classA&& obj) noexcept:innerClassB(obj.innerClassB)
	{
		//打断原来的指针,切记
		obj.innerClassB = nullptr;
		cout << "A类!!!移动!!!构造函数执行" << endl;
	}

static classA getA()
{
	classA a;//不直接return classA()就是为了让移动构造函数有机会执行
	return a;//如果有移动构造函数,就不会再去调用拷贝构造函数
}
int main()
{
	//执行的是拷贝构造函数,虽然有移动构造函数,但是你的代码没有不使用myClass1的倾向
	classA myClass1 = getA();
	//但是拷贝构造函数你完全可以移动内存
	classA myclass2(myClass1);

}

执行效果:
在这里插入图片描述
提出问题:
1、为什么A类执行析构函数的时候,作为A类的一个成员变量,B类没有执行析构函数呢?
这正是因为再函数getA()里面return的时候执行的是移动构造函数,而我们自定义的移动构造函数中,将B的所有权进行了交接,所以getA函数内部的A对象析构时,里面根本没有B的实例。
2、为什么我写了移动构造函数,但是classA myclass2(myClass1)这一句执行的却还是拷贝构造函数呢?
因为移动构造函数的参数是右值拷贝构造函数的参数是左值,而myClass1很明显是一个左值,如果想让他执行移动构造函数,很简单,把他用std::move()变成右值

int main()
{
	classA myClass1 = getA();
	classA myClass2(std::move(myClass1));
}

执行效果如下:
在这里插入图片描述

但是它毕竟还是调用了一个移动构造函数,我们完全可以再进行优化,直接不创建新的对象,给myClass1起一个别名:

	classA myClass1 = getA();
	classA&& myClass2(std::move(myClass1));

执行效果如下:
在这里插入图片描述

移动赋值运算符

移动赋值运算符的写法:

//移动赋值运算符,因为凡是带有“移动”概念的,都要部分摧毁传进来的对象,所以参数肯定不会带const
//并且都是右值,需要用std::move()传递进来
classA& operator=(classA&& obj)
{
	if (this == &obj)
	{
		return *this;
	}
	//斩断自己的内存
	delete innerClassB;
	//重新指向别人的内存
	innerClassB = obj.innerClassB;
	//别人斩断内存联系
	obj.innerClassB = nullptr;
	cout << "执行了类A的!!!移动赋值!!!运算符" << endl;
}
classA&& a = getA();	//一个构造函数,一个移动构造函数,一个析构函数
classA a2;				//一个构造函数
a2 = std::move(a);		//一个移动赋值运算符

执行效果如下:
在这里插入图片描述

合成的移动操作

某些条件下,编译器能合成“移动赋值运算符”,“移动构造函数”

  • 1、有自己的拷贝构造函数,拷贝赋值运算符,析构函数,编译器就不会生成“移动构造函数”,“移动赋值运算符”
    也就是说自己有拷贝对象,释放对象,编译器就不会合成
  • 2、没有移动函数的话,会用“拷贝构造函数”,“拷贝赋值运算符”来代替“移动构造函数”,“移动赋值运算符”
  • 3、只有当没有任何拷贝成员,,且所有的非静态成员都可以移动
    拷贝成员:拷贝赋值运算符,拷贝构造函数
    可以移动:内置类型,类类型要有相关移动相关的函数

满足以上,编译器就可以合成移动

	myClassC myClass1;
	myClass1.a = 99;
	myClassC myClass2 = std::move(myClass1);

统一的例子,简单明了,最后总结

用到的类myClassC

class myClassC
{
public:
	int a;
	char x[100];
public:
	myClassC() :a(100) ,x("I Love You")
	{
		cout << "执行了构造函数" << endl;
	};
	myClassC(const myClassC& obj)
	{
		a = obj.a;
		for (int i = 0; i < strlen(obj.x); i++)
		{
			x[i] = obj.x[i];
		}
		cout << "执行了拷贝构造函数" << endl;
	}
	myClassC(myClassC&& obj)
	{
		//delete x;
		//x = obj.x;
		//delete obj.x;
		cout << "执行了移动拷贝构造函数" << endl;
	}
	myClassC& operator=(const myClassC& obj)
	{
		a = obj.a;
		cout << "执行了赋值运算符函数" << endl;
		return *this;
	}
	myClassC& operator=(myClassC&& obj)
	{
		if (this == &obj)
		{
			return *this;
		}
		//delete x;
		//x = obj.x;
		//delete obj.x;
		cout << "执行移动赋值运算符" << endl;
		return *this;
	}
};
int main()
{
	myClassC myClass1;							//构造函数
	myClassC myClass2 = myClass1;				//拷贝构造函数
	myClassC myClass3 = std::move(myClass1);	//移动拷贝构造函数
	myClassC myClass4;
	myClass4 = myClass1;						//赋值运算符
	myClassC myClass5;
	myClass5 = std::move(myClass1);				//移动赋值运算符
}

执行结果
在这里插入图片描述

1、赋值运算符和拷贝构造函数的区别
拷贝构造函数移动拷贝构造函数都是 用一个对象去初始化一个类生成对象
赋值运算符移动赋值运算符都是 用一个对象去给另一个对象赋值

总结:
1、尽量给类添加移动构造函数,移动赋值运算符(占据大量内存的),因为临时对象都是右值,返回临时对象或者其他操作会调用很多移动构造函数和移动赋值运算符
2、noexcept加上,才能保证该调用移动的时候调用
3、移动完之后,之前的对象尽量delete
4、没有移动,会用拷贝代替

发布了157 篇原创文章 · 获赞 167 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_40666620/article/details/102953973