C++ 赋值的拷贝和移动语义

1. 类对象的默认赋值行为

class Demo1
{
public:
	Demo1(int a, int b, int c) : m_a(a), m_b(b), m_c(c) {}
public:
	int m_a;
	int m_b;
	int m_c;
};

ostream& operator<<(ostream& os, const Demo1& demo)
{
	os << demo.m_a << " " << demo.m_b << " " << demo.m_c;
	return os;
}

void test01()
{
	Demo1 demo1(10, 20, 30);
	Demo1 demo2(100, 200, 300);

	// 对象的初始化、赋值是不同的概念
	// 对象初始化只会发生一次,在对象创建的时候。
	// 对象赋值是对象创建完成之后(构造函数调用完成),才能进行赋值行为。可以发生多次。

	// 下列代码没有报错,说明类对象支持默认的赋值行为
	// 接下来,我们看一下,默认的赋值行为:1. 什么不做 2. 其他行为
	// 通过程序运行,我么发现默认的赋值行为:将 demo2 中的数据拷贝(逐字节)到 demo1
	demo1 = demo2;
	/*
		demo1.m_a = demo2.m_a;
		demo1.m_b = demo2.m_b;
		demo1.m_c = demo2.m_c
	*/
	cout << demo1 << endl << demo2 << endl;

	// 假设类的内部都是基本的数据类型,使用默认赋值行为是 OK 的。
	// 当类的内部一旦包含动态指针(指针成员,并且该成员指向堆空间),类的赋值行为就会变得复杂。
}

2. 类对象中深赋值和浅赋值问题

class Demo2
{
public:
	Demo2()
	{
		p_arr = new int[10];
		for (int i = 0; i < 10; ++i)
		{
			p_arr[i] = rand() % 100 + 1;
		}
	}

	// 针对使用左值给当前对象赋值
	Demo2& operator=(const Demo2& demo)
	{
		cout << "赋值运算符函数" << endl;

		// 赋值运算符函数实现的一般过程
		// 1. 对象判断,避免出现自身给自身赋值
		if (this == &demo)
		{
			return *this;
		}

		// 2. 释放当前对象 p_arr 指向的动态内存,避免内存泄漏【根据实际情况来定】
		if (p_arr != nullptr)
		{
			delete[] p_arr;
			p_arr = nullptr;
		}

		// 3. 重新按照 demo.p_arr 指向空间的大小给当前对象申请内存【根据实际情况来定】
		p_arr = new int[10];

		// 4. 将 demo.p_arr 指向的空间的数据拷贝到当前对象 p_arr 指向的内存
		// 也可以使用 memcpy 函数
		for (int i = 0; i < 10; ++i)
		{
			p_arr[i] = demo.p_arr[i];
		}


		// 5. 返回自身对象
		return *this;

		// 注意:如果能够确定无论 Demo2 创建出多少个对象,p_arr 指向的空间大小都是一样的,那么,第2、3步可以省略
		// 提高赋值效率。
	}

	// 针对右值给当前对象赋值
	Demo2& operator=(Demo2&& demo)
	{
		cout << "移动赋值运算符函数" << endl;

		// 1. 避免当前对象给自身赋值, 避免无用的操作
		if (this == &demo)
		{
			return *this;
		}

		// 赋值是对象创建完成之后进行的,对象的 p_arr 指针是指向动态内存
		if (this->p_arr != nullptr)
		{
			delete[] this->p_arr;
			this->p_arr = nullptr;
		}


		// 2. 将 demo 中的资源【移动】到当前对象身上
		this->p_arr = demo.p_arr;


		// 3. 将 demo 对象的 p_arr 指针设置为 nullptr, 避免出现动态内存多次释放
		demo.p_arr = nullptr;

		// 4. 将自身类型对象返回

		return *this;
	}

	~Demo2()
	{
		if (p_arr)
		{
			delete[] p_arr;
			p_arr = nullptr;
		}
	}

public:
	int* p_arr;
};


void test02()
{
	srand((unsigned int)time(nullptr));

	Demo2 demo1;
	Demo2 demo2;
	// 当类的内部包含动态指针,默认的赋值函数进行的是浅赋值,会导致两个问题:
	// 1. 被赋值对象的内存泄漏
	// 2. 赋值和被赋值对象重复指向同一个动态空间,导致动态空间多次释放
	// 解决方案:
	// 手动进行赋值过程的编写。
	// C++ 类对象提供了一种特殊的函数:赋值运算符函数,在该函数的内部编写赋值行为。
	// 当给类对象提供了赋值运算符函数,默认的赋值运算符函数就失效了。
	demo1 = demo2;   // Demo2& demo1.operator=(const Demo2& demo)
}

3. 类对象的移动赋值行为

当对象进行赋值的时,并不是所有的对象都需要完整的赋值过程(重新申请内存、数据拷贝)。
假设,赋值对象是:右值对象(匿名对象、将亡值对象)

void test03()
{
	Demo2 demo;
	// 1. 使用匿名对象给 demo 赋值
	// demo = Demo2();

	// 2. 将亡值对象 dead 给 demo 赋值
	Demo2 dead;
	demo = move(dead);  // move 函数将 dead 左值对象转换为右值对象
	cout << dead.p_arr << " " << demo.p_arr << endl;

	// 如何让对象具有移动赋值行为呢?
	// 移动赋值行为本质上还是赋值行为,无非赋值对象变成右值。
	// 所以,只需要给类对象增加赋值运算符函数即可,并且该函数的参数为右值引用即可。
	// Demo2& operator=(Demo2&&)
}
未经允许不得转载:一亩三分地 » C++ 赋值的拷贝和移动语义
评论 (0)

9 + 6 =