Margoo

...?

C++ 中的浅拷贝与深拷贝 铜牌收录

值语义和引用语义

在介绍深拷贝、浅拷贝以前,需要先了解 C++ 中的值语义(Value Semantics)和引用语义(Reference Semantics),先来说值语义,假设有两个对象 a,b,若 a = b 以后,b 的改变不会影响到 a,则说明这两个变量具有值语义。

其实,所有的原始变量(Primitive Variable)都具有值语义(原始变量包括即类似于 int 这种数据),又称为 POD(Plain Old Data),对于一个具有值语义的原始变量,变量赋值可以转换成内存的原内存拷贝(Bitwise Copy),所谓原内存拷贝,即为类似 memcpy 这样的拷贝,原始对象不需要调用 memcpy,只需要使用等号就可以复制内存,同时注意,STL 中大部分的模板类都是具有值语义的(如 std::string, std::vector, 其中也有反例如 std::function 和 std::thread)。

然而引用语义则变量保存的就是对值的引用,形如 a = b 的赋值不会拷贝一份新的副本,两个变量值占有一份内存空间,此时可以理解为这块内存空间的别名,或者是 a 的别名,最典型的例子就是指针。

说的更通俗一点就是,若有赋值语句 a = b 以后,b 的状态的改变依然能影响到 a 状态的改变,则说明这是一个引用语义。

深拷贝与浅拷贝

在上文介绍到了值语义与引用语义,现在我们再来讨论深拷贝与浅拷贝,假设有一个类 A 定义如下:

class A
{
public:
	~A()
	{
		delete ptr;
	}

public:
	bool Equal()
	{
		return value == *ptr;
	}

public:
	int  value;
	int* ptr;
};

此时并没有 A 声明构造函数与构析函数,编译器将会隐式为类 A 生成拷贝函数,一般来说,这个编译器生成的拷贝函数会把两个 A 的示例直接复制过来(注意是值上的复制),即为如下的形式:

A(const A& Instance)
{
	value = Instance.value;
	ptr   = Instance.ptr;
}

那么也就是说,编译器不会去管值语义与引用语义,这个赋值有可能是具有值语义的,可能是具有引用语义的;比如有下面这一行代码:

int main()
{
	A C;
	{
		A D;
		D.value = 1;
		D.ptr   = new int(1);

		C = D;
	}

	if (C.Equal())
	{
		return 0;
	}

	return 1;
}

这个时候问题就出现了,D 处于大括号内的作用域,而 A 处于大括号外的作用域,D 作为局部变量,超出作用域以后就会被销毁,因为 C = D 中,两个成员变量 ptr 之间的赋值是引用语义,而并非值语义,所以就会导致 D 调用构析函数以后销毁了 ptr 指针,而此时 C 中的 ptr 成员也受到了影响,最终就会导致 C.Equal() 返回的并不是 true 而是 false,这种拷贝形式我们就称之为浅拷贝。

那么深拷贝则就是当 C = D 的时候,C 中的 ptr 同 D 中的 ptr 指向了不同的内存,我们需要手动编写一系列构造函数如下所示:

A(const A& Instance)
{
	value = Instance.value;
	ptr = new int(*Instance.ptr);
}
A(A& Instance)
{
	value = Instance.value;
	ptr = new int(*Instance.ptr);
}
void operator=(const A& Instance)
{
	value = Instance.value;
	ptr = new int(*Instance.ptr);
}

此时的 ptr = new int(*Instance.ptr); 则就是一个拷贝构造了,再次运行上面的测试代码,C.Equal() 成功返回 true。

为什么编译器不实现深拷贝

这是因为深拷贝不可能实现,一个指针指向的对象有可能是一个数组,也有可能是多态,也可能需要更多的操作才能实现一次拷贝,这些问题都不是编译器能预料到的,所以只能交由用户自行设计。

事实上除了深拷贝和浅拷贝,C++ 中还有移动语义和拷贝交换以及移动构造这两个操作,他们同深浅拷贝有不同的引用场景,这些我们会在日后的文章再讨论。

添加评论