【C/C++】【类和对象】对象移动和移动构造函数

对象移动

C++11引入对象移动;进行所有权的转移;

移动构造函数和移动赋值运算符应该完成的功能

  • 确保必要的内存移动,斩断原对象和内存的关系;
  • 确保移动后原对象处于一种“即便被销毁也没有什么问题”的一种状态;A ---> B,确保不再使用A,而是应该去使用B;

移动构造函数

引入目的:提高程序效率;

说明:

  1. A --> B,那么A对象就不能再使用了;

  2. 移动:并不是把内存中的数据从一个地址移动到另一个地址,只是内存所有者变更;

拷贝构造函数

class Temp;
Temp::Temp(const Temp& tmp); //参数是const左值引用

移动构造函数

class Temp;
Temp::Temp(Temp&& tmp); //参数是右值引用
#include <iostream>
using namespace std;

class B
{
public:
	//默认构造函数
	B() :m_b(100) 
	{
		//cout << "类B的构造函数执行了" << endl;
	};
	//拷贝构造函数
	B(const B& tmp) :m_b(tmp.m_b)
	{
		//cout << "类B的拷贝构造函数执行了" << endl;
	};

	virtual ~B()
	{
		//cout << "类B的析构函数执行了" << endl;
	}
public:
	int m_b;
};


class A
{
public:
	A() :m_p_b(new B()) //调用类B的构造函数
	{
		cout << "类A的构造函数执行了" << endl;
	}
	
	A(const A& tmp) :m_p_b(new B(*(tmp.m_p_b))) //调用类B的拷贝构造函数
	{
		cout << "类A的拷贝构造函数执行了" << endl;
	}
	//C++11 引入 noexcept 通知标准库,移动构造函数不抛出任何异常,提高编译器工作效率;
	//移动构造函数要添加noexcept
	A(A&& tmp) noexcept :m_p_b(tmp.m_p_b) //原来对象a指向的内存m_p_b,直接就让这个临时对象指向这段内存;
	{
		cout << "类A的移动构造函数执行了" << endl;
		tmp.m_p_b = nullptr;
	}

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

public:
	B* m_p_b;
};


static A get_A()
{
	A a;
	return a; //生成临时对象 调用拷贝构造函数
}

int main()
{
	//B* p_b = new B(); //调用类B的构造函数;
	//p_b->m_b = 19;

	//B* p_b_1 = new B(*p_b); //调用类B的拷贝构造函数;


	//delete p_b; //析构函数
	//delete p_b_1; //析构函数


	A a = get_A(); //调用1次构造,1次拷贝构造,2次析构,程序结束


	//A中添加移动构造函数后,就调用移动构造函数
	//移动构造函数:把a对象移动给临时对象


	//A a = get_A(); 增加移动构造函数后,调用1次构造,1次移动构造,2次析构,程序结束
	A a_1(a); //1次拷贝构造函数

	//如何让A a_1(a) 调用移动构造函数
	//使用str::move
	A a_2(std::move(a));
	A&& a_2(std::move(a)); //不会产生新对象,不会调用移动构造函数;等同于把对象a有了一个新别名a_2; 后续建议使用a_2操作,不要再使用a;


	A&& aa = get_A(); //从getA返回临时对象被a接管了;//调用1次构造,1次移动构造,2次析构,程序结束
	return 0;
}

移动赋值运算符

#include <iostream>
using namespace std;

class B
{
public:
	//默认构造函数
	B() :m_b(100) 
	{
		//cout << "类B的构造函数执行了" << endl;
	};
	//拷贝构造函数
	B(const B& tmp) :m_b(tmp.m_b)
	{
		//cout << "类B的拷贝构造函数执行了" << endl;
	};

	virtual ~B()
	{
		//cout << "类B的析构函数执行了" << endl;
	}
public:
	int m_b;
};


class A
{
public:
	A() :m_p_b(new B()) //调用类B的构造函数
	{
		cout << "类A的构造函数执行了" << endl;
	}
	
	A(const A& tmp) :m_p_b(new B(*(tmp.m_p_b))) //调用类B的拷贝构造函数
	{
		cout << "类A的拷贝构造函数执行了" << endl;
	}
	//C++11 引入 noexcept 通知标准库,移动构造函数不抛出任何异常,提高编译器工作效率;
	//移动构造函数要添加noexcept
	A(A&& tmp) noexcept :m_p_b(tmp.m_p_b) //原来对象a指向的内存m_p_b,直接就让这个临时对象指向这段内存;
	{
		cout << "类A的移动构造函数执行了" << endl;
		tmp.m_p_b = nullptr;
	}

	virtual ~A()
	{
		delete m_p_b;
		cout << "类A的析构函数执行了" << endl;
	}
	//拷贝赋值运算符
	A& operator=(const A& src)
	{
		if (this == &src)
			return *this;
		delete m_p_b; //释放自己的这块内存
		m_p_b = new B(*(src.m_p_b)); //重新分配一块内存
		cout << "类A的拷贝赋值运算符执行了" << endl;
	}
	//移动赋值运算符
	A& operator=(A&& src) noexcept
	{
		if (this == &src)
			return *this;

		delete m_p_b; //释放自己的这块内存
		m_p_b = src.m_p_b; //指向对方指向的内存
		src.m_p_b = nullptr; //对方置空
		cout << "类A的移动赋值运算符执行了" << endl;
	}
public:
	B* m_p_b;
};


static A get_A()
{
	A a;
	return a; //生成临时对象 调用拷贝构造函数
}

int main()
{
	A a = get_A(); //1个构造函数,1个移动构造函数,1个析构函数
	A a_1;  //1个构造函数

	a_1 = std::move(a); //调用移动赋值运算符
	return 0;
}

合成的移动操作

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

  1. 有自己的拷贝构造函数,自己的拷贝赋值运算符,或者自己的析构,那么编译器就不会为它合成移动构造函数和移动赋值运算符
  2. 如果没有自己的移动构造函数和移动赋值运算符,系统会调用我们自己写的拷贝构造函数和拷贝赋值运算符来代替;
  3. 只有一个类没有定义自己的拷贝构造成员(拷贝构造函数和拷贝运算符),且类中的每个非静态成员都可以移动时,编译器才会为该类合成移动构造函数或者移动赋值运算符;

什么叫做成员可以移动?

  1. 内置类型可以移动;

  2. 类类型成员,如果这个类有对应的移动操作相关的函数,就可以移动;

    //举例
    struct TC
    {
    	int i; //内置类型可以移动
    	string a; //string类中定义了自己的移动成员
    }
    
    int main()
    {
        TC a;
        a.i = 100;
        a.s = "Hello world";
        const char *p = a.s.c_str();
        
        TC b = std::move(a); //导致TC类的合成移动构造函数(编译器生成的)执行;
        const char *q = b.s.c_str();
        
        //p和q的地址不一样,由于string对象的特性决定的;
        //这种移动,不是真正的移动,只是拷贝;
        return 0;
    }
    

总结

  • 尽量给类增加移动构造函数和移动赋值运算符;
  • noexcept;
  • 记得将被移动对象赋值给nullptr,让被移动对象随时处于一种能够被析构的状态;
  • 没有移动,会调用拷贝代替;
posted @ 2020-07-22 11:05  NaughtyCoder  阅读(289)  评论(0)    收藏  举报