【iOS】深拷贝与浅拷贝

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

在前面学习NSString的三种实现方式的过程中,遇到了深浅拷贝的问题,特此撰写博客总结知识

一、定义

浅复制是指创建一个新对象,该对象与原始对象共享内部数据的引用。换句话说,浅复制只复制对象本身,而不复制对象所引用的数据。因此,原始对象和副本对象指向同一块内存,对其中一个对象的修改会影响另一个对象。
通俗的讲,浅复制就是仅仅创建一个存放与复制对象相同地址的指针变量,也可以称为指针拷贝

深复制是指创建一个新对象,并复制原始对象及其引用的所有数据。深复制会递归地复制对象的所有内容,包括对象所引用的其他对象。这样,原始对象和副本对象拥有独立的内存空间,彼此之间的修改互不影响。
通俗的讲,深复制就是创建一个与被复制的对象的值完全相同的对象,但是他们的地址是不同的,因此深复制也可以称为内容拷贝

二、非容器类对象的深浅拷贝

我们以我们的代码来解释我们的拷贝原理:

NSString:

 NSString *string1 = @"helloworld";
 NSString *string2 = [string1 copy];
 NSString *string3 = [string1 mutableCopy];
 NSMutableString *string4 = [string1 copy];
 NSMutableString *string5 = [string1 mutableCopy];
 NSLog(@"string1 = %p",string1);
 NSLog(@"string2 = %p",string2);
 NSLog(@"string3 = %p",string3);
 NSLog(@"string4 = %p",string4);
 NSLog(@"string5 = %p",string5);

编译结果:
在这里插入图片描述

NSMutableString:

NSMutableString *string1 = [NSMutableString stringWithString:@"helloworld"];
NSString *string2 = [string1 copy];
NSString *string3 = [string1 mutableCopy];
NSMutableString *string4 = [string1 copy];
NSMutableString *string5 = [string1 mutableCopy];
NSLog(@"string1 = %p",string1);
NSLog(@"string2 = %p",string2);
NSLog(@"string3 = %p",string3);
NSLog(@"string4 = %p",string4);
NSLog(@"string5 = %p",string5);

在这里插入图片描述
结论:
从我们上述的结果可以看出:
对于不可变非容器类对象(NSString),它的copy是浅拷贝,mutablecopy是深拷贝
对于可变容器类对象(NSMutableString),它的copy与mutablecopy都是深拷贝

引用:
在这里插入图片描述
这里引用一下其他博主的结论,可以知道我们复制后的对象的类型只与深还是浅复制有关,而与原来的数据类型无关

三、容器类对象的深浅拷贝

NSArray:

 NSArray *array = [NSArray arrayWithObjects:@"a",@"b",@"c", nil];
 NSArray *arrayCopy = [array copy];
 NSArray *arrayMutableCopy = [array mutableCopy];
 NSArray *tempArray = array[0];
 NSArray *tempArrayCopy = arrayCopy[0];
 NSArray *tempArrayMutableCopy = arrayMutableCopy[0];
 NSLog(@"不可变数组  copy 和 mutableCopy的区别");
 NSLog(@"                对象地址     对象指针地址  firstObject地址  firstObject指针地址");
 NSLog(@"      array: %p , %p , %p , %p", array, &array, tempArray, &tempArray);
 NSLog(@"       copy: %p , %p , %p , %p", arrayCopy, &arrayCopy, tempArrayCopy, &tempArrayCopy);
 NSLog(@"mutalbeCopy: %p , %p , %p , %p", arrayMutableCopy, &arrayMutableCopy, tempArrayMutableCopy, &tempArrayMutableCopy);

编译结果:
在这里插入图片描述

NSMutableArray:

NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"a",@"b",@"c", nil];
NSMutableArray *mutableArrayCopy = [mutableArray copy];
NSMutableArray *mutableArrayMutableCopy = [mutableArray mutableCopy];
NSMutableArray *tempMutableArray = mutableArray.firstObject;
NSMutableArray *tempMutableArrayCopy = mutableArrayCopy.firstObject;
NSMutableArray *tempMutableArrayMutableCopy = mutableArrayMutableCopy.firstObject;
NSLog(@"可变数组  copy 和 mutableCopy的区别");
NSLog(@"                 对象地址     对象指针地址  firstObject地址  firstObject指针地址");
NSLog(@"mutableArray: %p , %p , %p , %p", mutableArray, &mutableArray, tempMutableArray, &tempMutableArray);
NSLog(@"        copy: %p , %p , %p , %p", mutableArrayCopy, &mutableArrayCopy, tempMutableArrayCopy, &tempMutableArrayCopy);
NSLog(@" mutalbeCopy: %p , %p , %p , %p", mutableArrayMutableCopy, &mutableArrayMutableCopy, tempMutableArrayMutableCopy,
&tempMutableArrayMutableCopy);

编译结果:
在这里插入图片描述
结论:
1、copy和mutableCopy之后对象地址的变化与非集合对象变化相同。不可变类copy是浅拷贝,mutablecopy是深拷贝,可变类全都是深拷贝
2、无论是copy还是mutableCopy,内部元素的地址都没有变化,只是指针地址发生了变化。

由此我们总结一下,我们的容器类与非容器类对深浅拷贝不尽相同:
对于容器而言,元素对象始终是指针复制。 也就是说数组中的元素的拷贝始终是浅拷贝。

这里值得注意的是,即使是可变数组的深拷贝,里面的元素的地址还是原来元素的地址,只是在我们的内存中开辟了一块新空间存储新对象,也可以称其为浅层的深拷贝

Tip:
这里解释一下为何数组中的元素始终是浅拷贝:

在这个过程中,我们知道数组的元素实际上是对象的引用。所以,当我们访问数组的第一个元素时,不管是从原始数组 array、拷贝的不可变数组 arrayCopy 还是拷贝的可变数组 arrayMutableCopy 中访问,都会得到同一个字符串对象的引用。

因此,这三个变量 tempArray、tempArrayCopy 和 tempArrayMutableCopy 的地址是相同的,因为它们指向的是同一个字符串对象。这里的地址是字符串对象的内存地址。

我们再仔细来理解一下这句话:数组的元素实际上是对象的引用

这句话的意思是,在数组中存储的元素实际上是指向对象的引用,而不是对象本身。当我们将一个对象添加到数组中时,实际上是将该对象的引用存储在数组的对应位置上。
我们通过以下示例来理解这句话

NSString *string1 = @"Hello";
NSString *string2 = @"World";

NSArray *array = @[string1, string2];

在这个例子中,array 是一个包含两个元素的数组。每个元素都是一个 NSString 对象。但是,数组中存储的并不是字符串对象本身,而是指向这些字符串对象的引用。

这意味着当我们修改原始的字符串对象时,数组中对应的元素也会随之改变,因为它们引用同一个对象。例如:

string1 = @"Hello World";
NSLog(@"%@", array[0]);  // 输出结果为 "Hello World"

在这个示例中,我们修改了 string1 的值,将其指向了新的字符串对象 “Hello World”。由于数组 array 中的第一个元素实际上是指向 string1 的引用,所以输出结果变为了 “Hello World”。

这就是所说的数组的元素实际上是对象的引用,而不是对象本身。这种引用机制使得多个变量可以引用同一个对象,从而实现对象之间的共享和交互。同时,需要注意当对象被修改时,所有引用该对象的地方都会受到影响。

四、自定义类对象的深浅拷贝

我们的自定义类中没有实现我们的NSCopy等协议,我们需要手动实现它

协议部分:
在这里插入图片描述
接口部分:
在这里插入图片描述
实现部分:
在这里插入图片描述

主函数:
在这里插入图片描述
编译结果:
在这里插入图片描述

因此可以知道,我们自定义类的copy与mutablecopy都是深拷贝

五、属性关键字的深浅拷贝

我们对我们的自定义类中添加一些代码,形成如下的代码
在这里插入图片描述

我们分strong与copy对其属性进行讨论

strong:
在这里插入图片描述

编译结果:

在这里插入图片描述
copy:
在这里插入图片描述
编译结果:
在这里插入图片描述

因为我们创建的string是可变的,我们对其t使用appendstring方法时,用strong修饰的可以被修改,用copy修饰的不能被修改

因此我们可以知道,copy修饰的属性在进行赋值时实际上是进行了深拷贝,创建了一个新的对象,所以没有被修改
strong修饰的属性在复制时则是进行了浅拷贝,所以能被修改

使用copy修饰不会发生改变的具体原因是:copy修饰下,我们创建新的字符串时属于深拷贝,会拷贝出一个新的对象,当我们改变t的值的时候,改变的只是string这个对象自己,t这个新对象并不会受到影响,所以值并不会改变

使用strong修饰会发生改变的具体原因是:strong修饰下,我们的t会持有原来的对象,使原来的对象引用计数+1,属于浅拷贝,这时候我们进行操作时,sting和t的值都会发送改变

所以我们得出结论:
strong的复制是多个指针指向同一个地址(浅拷贝),而copy的复制是每次会在内存中复制一份新的对象(深拷贝),其中对象的值和原对象相同而地址不同,一般来说对于不可变对象我们使用copy修饰,而可变的对象使用strong修饰

注意的点:在程序中,只有用点语法才能使让属性关键字发挥作用

下面粘贴学长博客中的图来帮助我们的理解:
在这里插入图片描述

六、容器类对象的完全深拷贝

我们在上文讲述容器类对象的深浅拷贝的时候,提到了我们的容器类对象的深拷贝实际上是一种浅层深拷贝

它的意思是我们的深拷贝仅仅创建了一个新的容器类对象,里面的元素仅仅只是进行了指针拷贝。

通俗的讲,就是我们容器类对象的深拷贝仅仅只是创建的新容器对象地址不同,里面的元素地址仍然是相同的

为此我们可以试着去实现完全深拷贝,也就是让其里面元素也进行内容复制

下面介绍实现深拷贝的两种方法:

1、copyItems

我们先来给出它的定义:
copyItems: 方法是 NSArray 和 NSMutableArray 的一个方法,用于创建一个新的数组,并对数组中的元素进行拷贝。该方法会遍历原始数组的每个元素,并对每个元素执行 copy 操作,然后将拷贝后的元素添加到新的数组中

可以看到定义中的关键是遍历数组中的每个元素对其进行copy,这里需要注意的是如果集合中的对象有自定义类,那么使用这个方法前自定义类必须实现NSCopying协议,否则会报错

接下来我们给出代码:

        Test *t = [[Test alloc] init];
        t.name = @"牢大";
        
        NSArray *p =[NSArray arrayWithObject:t];
        NSArray *tt = [[NSArray alloc] initWithArray:p copyItems:YES];
        
        NSLog(@"%@", p);
        NSLog(@"%@", tt);
        
  		NSLog(@"%@", p[0]);
        NSLog(@"%@", tt[0]);

打印结果:
在这里插入图片描述

这里可以看到我们创建了一个p数组,里面存放的是自定义类对象,我们创建一个新数组tt对其使用copyItems:方法进行初始化,可以看到tt与p的地址不同,同时他们内部元素的地址也不同,这就说明:如果容器类对象中的元素是非容器类,那么我们可以用copyItems:方法对其进行完全深复制

这里为什么笔者要强调是容器类对象中的元素是非容器类,因为这个方法实际上有着一定的局限性:
当容器类对象中的元素是容器类对象时,那么就无法实现完全深拷贝

我们给出例子:

Test *t = [[Test alloc] init];
t.name = @"牢大";

NSArray *p =[NSArray arrayWithObject:t];
NSArray *k = [NSArray arrayWithObject:p];
NSArray *tt = [[NSArray alloc] initWithArray:k copyItems:YES];

NSLog(@"%@", k);
NSLog(@"%@", tt);

NSLog(@"%@", p[0]);
NSLog(@"%@", k[0]);
NSLog(@"%@", tt[0]);

输出结果:
在这里插入图片描述

这里我们可以看到,当我们容器类对象中的元素是容器类对象时,我们对其使用copyItems:方法,两个对象的地址相同,仅仅实现了浅拷贝。

因此我们可以得出结论:

使用 copyItems:
方法进行深复制仅适用于数组的直接元素,即数组中的对象。如果数组的元素还是容器对象(如数组、字典等),那么仅对容器对象本身进行拷贝(而且还是浅拷贝),并不会对容器内部的元素进行深度拷贝。这意味着容器对象内部的元素仍然是共享的,修改其中一个数组的元素可能会影响到另一个数组。

2、递归复制属性

那么为了解决这个问题,我们可以对我们重写的copyWithZone:方法中的属性进行递归复制

代码如下:
在这里插入图片描述

主函数部分:
在这里插入图片描述

打印结果:
在这里插入图片描述

由此我们实现了完全的深复制。

3、解档与归档
这个方法笔者并没有尝试,这里给出讲解:

归档和解档(Archiving and Unarchiving):

使用归档和解档的方式可以将对象及其属性序列化为二进制数据,然后通过解档重新创建一个全新的对象。

这种方式要求被拷贝的对象及其属性必须遵循 NSCoding 协议,实现 initWithCoder: 和 encodeWithCoder: 方法。

实现深拷贝的步骤如下:
使用 NSKeyedArchiver 的 archivedDataWithRootObject: 方法将原始对象归档为二进制数据。
使用 NSKeyedUnarchiver 的 unarchiveObjectWithData: 方法将二进制数据解档为全新的对象。

示例代码:
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject:originalObject]; id deepCopyObject = [NSKeyedUnarchiver unarchiveObjectWithData:archivedData];

总结

1、浅复制是指针拷贝,深复制是内容拷贝

2、非容器类对象的深浅复制:
不可变对象的copy是浅复制,mutablecopy是深复制
可变对象的copy与mutablecopy都是深复制

3、容器类对象的深浅复制
不可变对象的copy是浅复制,mutablecopy是深复制
可变对象的copy与mutablecopy都是深复制
容器中的元素的复制全都是浅复制
需要注意的一点是对原对象添加元素时,浅复制的对象的内容也会随之改变,而深复制不会

4、自定义类对象的深浅复制:
必须实现NSCopying协议,且全都是深拷贝,但是是浅层的深拷贝

5、属性关键词的深浅复制:
strong修饰的属性一般是可变对象,copy修饰的属性一般用于不可变对象,strong实际上是对对象的属性进行了一次浅拷贝,对象的内容修改时strong修饰的属性也会随之变化,而copy则相当于进行了一次深拷贝,内容不会随之变化

6、容器类对象的完全深拷贝

  1. copyItems
    当容器中元素不是容器类对象时,可以实现完全深拷贝
  2. 递归复制
    用于容器中元素是容器类对象时,就必须使用这种方法
  3. 解档与归档
    必须实现NSCoding方法,仅进行了了解

猜你喜欢

转载自blog.csdn.net/weixin_72437555/article/details/130834791