深拷贝与浅拷贝

在《C语言容易忽略的知识点》一文中,有读者说这种结构体复杂成员赋值的的拷贝是浅拷贝(感谢读者提出),那么到底什么是深拷贝,什么是浅拷贝?

浅拷贝

浅拷贝指的是仅拷贝对象的所有成员,而不包括其引用对象(例如指针指向的其他内容)。我们来看C和C++的例子。

C++的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//来源:公众号【编程珠玑】
//https://www.yanbinghu.com
#include <iostream>
class Test
{
public:
/*构造函数*/
Test():a(0)
{
std::cout<<"new b"<<std::endl;
b = new char[16];
}
/*析构函数*/
~Test()
{
std::cout<<"delete b"<<std::endl;
delete [] b;
}
private:
int a;
char *b;
};
int main()
{
Test test;
Test test0 = test;//浅拷贝
return 0;
}

运行结果:

1
2
3
4
new b
delete b
delete b
core dumped

可以看到,在拷贝test的时候,只拷贝了其成员本身的值,即a和b的值,而b只是一个指针,它指向的内容却没有被拷贝,因此我们说,它是浅拷贝。而对象test在被销毁时,会释放b指向的内存,test0被销毁时,又进行了重复释放,因此导致core dumped。

其拷贝过程如下图所示:

浅拷贝

C语言版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//来源:公众号编程珠玑
//https://www.yanbinghu.com
#include<stdio.h>
#include<stdlib.h>
typedef struct Member_t
{
char *p;
int c;
}Member;
typedef struct Test_t
{
int a;
Member b;
}Test;
int main(void)
{
Test test0;
test0.a = 10;
test0.b.p = malloc(16);
if(NULL == test0.b.p)
{
printf("malloc failed\n");
}
snprintf(test0.b.p,16,"hello");
test0.b.c = 24;
/*拷贝*/
Test test1;
test1.a = test0.a;
test1.b = test0.b;
/*修改*/
snprintf(test1.b.p,16,"world");
printf("%s\n",test0.b.p);
free(test0.b.p);
test0.b.p = NULL;
return 0;
}

运行结果:

1
world

由于其结构体成员赋值时,只拷贝其成员本身的值,即

1
test1.b = test0.b

只拷贝了其中的p的值和c的值,却没有拷贝p指向的内存,因此拷贝之后,两者的p指向同一片内存区域,导致通过其中一个修改就会影响另外一个的内容。因此它也是浅拷贝。(感谢在上篇中读者指出)

深拷贝

深拷贝除了拷贝其成员本身的值之外,还拷贝的成员指向的动态内存区域等类似的内容。
那么对于前面的例子,我们如何进行深拷贝呢?以C++为例,我们需要定义自己的拷贝构造函数:

1
2
3
4
5
6
7
8
Test(Test &t)
{
std::cout<<"copy"<<std::endl;
a = t.a;
b = new char[16];
/**拷贝b指向的内容**/
memcpy(b,t.b,16);
}

这里就不是拷贝指针b的值,而是拷贝指针b指向的内容。因此是深拷贝。再次运行结果:

1
2
3
4
new b
copy
delete b
delete b

这种情况下,test和test0中b的值是不一样的,但是b指向的内容是一样的。

那么C语言中怎么处理呢?自然就是需要拷贝成员b中p指向的内容了。这里就留给读者自己去实现了。

深拷贝过程如下:

深拷贝

C语言里的深拷贝与浅拷贝

作为使用C语言的读者来说,我觉得到没有必要去抓什么深拷贝与浅拷贝的概念,你只需要理解,C里面的赋值类的拷贝,仅仅是拷贝值而已,比如你拷贝的是指针,那么只是拷贝指针的值,指针指向的区域是不会拷贝的;而如果你拷贝的是数组,那么将会拷贝数组的值,而不是数组首地址(参考《C语言容易忽略的知识点》中的例子)。

结构体赋值

那么回到结构体赋值成员赋值的问题。根据上面的分析可以知道,如果结构体成员都是基本数据类型或者数组(非指针),那么直接赋值是没有任何问题的,而且非常地方便,而如果成员有指针类型,你又不想复制的结构体成员指向相同的内存区域,那么你就需要自己拷贝其指向的内容。

关于数组和指针,请参考《数组之谜》。

总结

默认的拷贝行为基本都是浅拷贝,即仅仅拷贝其成员值。当然如果所有成员值没有引用任何外部对象,或者引用的外部对象定义了自己的深拷贝行为,那么深拷贝和浅拷贝是一样的。如果需要拷贝值以外的内容,请自己定义拷贝行为。

最后,一张图理解深拷贝和浅拷贝:
深拷贝和浅拷贝

最后关于C语言,自动动手,丰衣足食。

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!