最近对深浅拷贝(复制)做了一些研究,在此将自己的理解写下来,希望对大家有所帮助。本人尚处在摸索阶段,希望各位予以指正。
本文包括如下方向的探索:
1.指针与对象;
2.深/浅拷贝(复制);
3.可变/不可变对象;
4.Objective-C中的copy与mutableCopy方法。
一.指针与对象
在初始学习编程的时候,对于绝大多数面向对象编程的语言,这都是个绕不开的重点与难点。非常惭愧的是对它的认识我一直是不够的,并且感觉这项技术有许多的内容可以挖掘。说这是面向对象编程的核心思想也不为过。很多程序员因为日常中没有用到指针对象的高级用法而就不去思考,我认为是非常危险的。
我先摘录下百度百科的介绍:
指针:在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。在高级语言中,指针有效地取代了在低级语言,如汇编语言与机器码,直接使用通用暂存器的地方,但它可能只适用于合法地址之中。指针参考了存储器中某个地址,通过被称为反参考指针的动作,可以取出在那个地址中存储的值。
对象(广义):在内存上一段有意义的区域,称作为一个对象。
我先简单地说一下我的理解:
我们知道我们所有的程序都是在系统内存之中运行的。这个内存也就是我们常说的4G/8G/16G的东西,它虽然相对硬盘很小,但是极其影响系统整体性能的东西。这里就不过多展开了。
而对象,其本质就是我们在内存中声明并使用的一段内存空间。这段内存空间的内容大小是我们决定的;我们使用这段内存空间存储一个实例化的值。
对于指针,它的本质仍然是一段内存空间;但是这段内存空间的大小和存储的内容确是固定的;我们使用指针这一段内存空间存储一个内存地址,而这段内存地址指向的位置,就是这个指针指向的对象的位置。这也就是指针被称为“指针”的原因。
依然非常抽象。但结合实际例子就会有完全不一样的理解了。
下面以NSArray这个对象为例,说明下在我们实际用法中关于指针与对象的使用。
NSArray *array = [[NSArray alloc] init];
像类似的 Class *instance = [[Class alloc] init]; 我们每天都在用。但这其中就包含着指针与对象的思想。
NSArray *array;
这其实就是声明了一个指针。这个指针所在的内存空间已经被创建好了,并且已经将这个指针放置在这段内存空间中了;这段内存空间中存储的内存地址还没有被设置,但可以确定的是这段内存地址所指向的对象必须是 NSArray 类型的。
可能会疑惑的是,我这么简单的操作居然做了这么多事情吗?这其实就是高级语言的优点:封装。它将繁琐复杂的对机器编程的语言给逐步封装成为了人能一眼看懂具体做了什么操作的语言。从此我们不再需要声明一个指针还需要去记住繁琐的机器语言指令,只要声明一下就可以了。
但这并不代表汇编语言不重要,如果有时间我建议大家都要有所了解。
好了,既然前面是指针,那后面的操作是什么呢?
[[NSArray alloc] init];
这个返回值,其实就是一个对象。它符合我们对对象的所有定义。
分开来看,我们其实做了两步操作,分别对应着我们需要系统内存做的两件事:
1.alloc:在系统内存中声明空间。其大小由我们控制。当然在实际情况中空间的大小已经被封装的很好了,你只需要知道我们已经有了这样的内存空间就可以了;
2.init:有了空间还不够,我们究竟要在里面存储些什么呢?这就需要init方法了:我们要在其中存储一个我们规定好的实例。什么样子呢?那就需要你在init方法中自定义了,是这个样子那个样子的实例。
对于指针与对象,这就是我的理解。
另外还有一件事情,那就是我们还可以这样去声明一个对象:
NSArray *array = [NSArray array];
这怎么解释呢?
其实这很好解释。我们可以试着重写一下它的这个方法:
+ (instancetype)array { return [[NSArray alloc] init];
}
我的意思是:不过你是通过什么方法实例化的一个对象,只要不是copy出来的,你都逃不过会经过alloc、init这两个方法。这两个方法才是对象的核心初始化方法。
关于指针与对象就说到这里。
二.深/浅拷贝(复制)
刚才我们研究了指针与对象。但其实本文的核心是研究下深/浅复制。为什么要先讲指针与对象呢?因为深/浅复制的本质区别,就是要不要复制对象的区别。
先上概念:
浅复制:只针对指针的复制。复制之后是新旧两个指针指向同一个对象,分别根据指针的类型(strong、weak、assign)瓜分对象的所有权。
深复制:不止复制指针,还重新申请了内存空间,将对象的数据拷贝了一份赋值给新开辟的空间里。复制结果是:新指针指向新对象,旧指针指向就对象;新旧对象之间数值完全相同,但新旧之间互不干扰。所以也就不存在对就对象的内存管理问题。
简单来说,浅复制就只复制了指针,而深复制是指针对象一起复制了一份。
三.可变与不可变对象
为什么这个也要单独拿出来提一下呢?
我们刚才说过,对于对象而言,它占有的内存空间大小是我们决定的;而这块内存空间的大小实际上又是alloc这个方法决定的。但alloc不仅仅是决定空间的大小,它还包含着声明新的内存空间的功能;也就是说,这块内存空间的大小,我们只能在实例化一个对象的时候决定,其他时候不能更改。
那么如果我们必须要在其他时候修改它的占用空间的大小呢?难道就没有办法了吗?
办法自然是有的。为了解决这个问题,Objective-C特别提出了可变对象(MutableObject)这个概念。也就是说,可变对象所占用的内存大小不仅仅可以在初始化alloc的时候决定,还可以在其他的时候由它自己决定。这就是可变对象与不可变对象的区别。
了解了这个概念之后,我们回过头再看Objective-C的Foundation框架:这里面有提供什么样的可变对象类型呢?
应该说,带有mutable字样的,我们都需要考虑这是不是一个可变类型对象。
常见的,也就是NSMutableArray、NSMutableDictionary、NSMutableString、NSMutableAttributedString这一些了。
还有很多,就不一一列举了。
经过上面的介绍,我想大家对我们常用的NSMutableArray和NSMutableDictionary都有了更新的认识。它们相对于NSArray与NSDicitonary的区别在于什么?区别就在于它能不断地修改自己的内存空间的大小,用来存储更多或者更少的值。
更详细一点呢,就要谈到数组(字典类似)的本质了:数组本身就是一串内存空间;它们除了在首位置存储的是关于数组字典本身的数据之外,后面的内存空间存放的都是内存地址,也就是数组存储的对象所在的内存地址;用C语言来形容的话呢,Objective-C中的数组就是指针数组。
这样,可变数组与不可变数组的区别,就在于不可变数组的内存空间大小是不能变的,只能存储这样多的数据,也就是只能查,不能增删改;而可变数组就是可以改变自己的内存空间大小,这样就可以完成对数组中对象的增删改查操作了。
我还想仔细介绍下NSMutableString这个对象。这个对象的出现频率极少。它也仅仅比NSString多了一个方法:
我觉着这个方法就不用过多介绍了,就是字面上的意思。但这也体现出NSMutableString的特色了。我可以修改自己所占用的内存空间的大小啊~
同时,除了打印对象类型,我们还可以通过是否实现这个方法来确定当前对象究竟是NSString还是NSMutableString类型。
四.Objective-C中深/浅拷贝的用法:copy与mutableCopy
终于到了使用上面所有的概念的时候了。我们来看一下在Objective-C语言中实际对可变/不可变对象进行深/浅拷贝的操作结果。
1.关于copy与mutableCopy这两个方法的适用范围
像这样用:
那这两个方法是继承自谁呢?
同时我们刚才看的非常清楚,介绍上显示的是 Return the object returned by mutableCopyWithZone: where zone is nil。对于copy,那就是 copyWithZone: 这个方法了。这两个方法在这里。
同时告诉我们这个方法在ARC下不适用。
哪里不适用了?我们不但能敲出来,而且不报错!
但是点进去看我们就发现区别了:
并不是我们刚才那两个方法,而是NSCoping和NSMutableCoping协议中的方法。
我们可以验证一下:刚才的 copyWithZone: 是NSObject的方法:
果然没有这个方法了。
那么NSArray为什么会有呢?
也很简单,因为NSArray遵守了NSCoping和NSMutableCopying这两个协议并实现了这两个方法。
我们再来看介绍,只有遵守了 NSCoping 协议的类型,才可以执行copy这个方法。其他的会崩溃。例如这样:
以后想知道一个类可不可以执行copy与mutableCopy方法,去它的类型里面查看是否遵守NSCopying与NSMutableCopying协议即可。
2.Objective-C中针对可变/不可变类型的对象 copy与mutableCopy 的效果
我们可以看到,遵守 NSCopying 协议的类,一般也会遵守 NSMutableCopying 协议;而遵守这两个协议的类,一般有与之对应的 可变/不可变的 另一个类。
例如:
NSString - NSMutableString; NSArray - NSMutableArray; NSDictionary - NSMutableDictionary.
下面以NSString举例说明 可变/不可变对象 执行 copy/mutableCopy 之后的结果。
2.1 NSString
我写了这样一段代码:
/*********** 对于不可变对象(NSString)的测试 ***********/
NSString *string = @""; NSString *string1 = string; // string1与string地址相同 NSString *stringCopy = [string copy]; // stringCopy与string 地址相同 NSString *stringMutableCopy = [string mutableCopy]; // stringMutableCopy与string地址不同
NSLog(@"\n%p\n%p\n%p\n%p", string, string1, stringCopy, stringMutableCopy); // 带&则打印的是指针的地址;不带&打印的是指的对象的地址。
ps:需要注意的是, %p 占位符表示打印内存地址;后面直接放置指针表示打印指针指向的对象的地址,也就是指针的内存空间存储的内存地址;如果是 &指针 则表示打印的是指针的地址。不要搞混。
打印结果:
我直接上结论了:
/**
结论1:
1.对于不可变对象,“直接声明新指针并利用旧指针进行指针赋值”与“声明新指针并指向对象的copy”的效果完全相同,是浅拷贝,复制“指针”不复制“对象”,指向的对象为同一个;
2.但是需要注意此时并不能通过直接修改指针指向的字符串进行字符串对象的修改,因为修改指针指向的字符串的本质是更换了指针指向的对象,而不是对对象进行修改;
3.mutableCopy则是对象与指针共同复制,分配新的内存地址,并用新的指针指向它。
*/
这是我在运行之后自己总结的结论。总结来说,就是对于不可变对象,copy操作是浅复制,mutableCopy是深复制。
但其实仍然有若干问题;比如下面的问题:
NSLog(@"\n%@\n%@\n%@\n%@", NSStringFromClass([string class]), NSStringFromClass([string1 class]), NSStringFromClass([stringCopy class]), NSStringFromClass([stringMutableCopy class]));
我又打印了四个指针指向的字符串的类型。其实前三个在刚才已经证明了它们指向的是一个对象,所以类型肯定是一样的;但是mutableCopy得到的字符串究竟是什么类型呢?
对于这个__NSCFConstantString和__NSCFString,可以理解为在编译器解析的时候产生的类型,或者说也可以理解为是NSString和NSMutableString在它们之上进行了一次封装。不管怎么理解,我们都可以发现,NSString 类型的字符串在执行 mutableCopy 之后竟然变成了 NSMutableString 类型。这是什么情况?我们来验证一下:
NSMutableString *mutableString = (NSMutableString *)stringMutableCopy;
[mutableString replaceCharactersInRange:NSMakeRange(, ) withString:@"HAHAHA"];
NSLog(@"%@", mutableString);
没有任何问题。
我们可以得出结论:对于不可变对象,mutableCopy不仅仅是深复制,返回的对象类型还是不可变对象类型相应的可变对象的类型。
我们可以换NSArray验证一下,完全成立。
/*********** 对于不可变对象(NSString)的测试 ***********/
NSString *string = @[@""]; NSString *string1 = string; // string1与string地址相同 NSString *stringCopy = [string copy]; // stringCopy与string 地址相同 NSString *stringMutableCopy = [string mutableCopy]; // stringMutableCopy与string地址不同
NSLog(@"\n%p\n%p\n%p\n%p", string, string1, stringCopy, stringMutableCopy); // 带&则打印的是指针的地址;不带&打印的是指的对象的地址。 NSLog(@"\n%@\n%@\n%@\n%@", NSStringFromClass([string class]), NSStringFromClass([string1 class]), NSStringFromClass([stringCopy class]), NSStringFromClass([stringMutableCopy class])); NSMutableString *mutableString = (NSMutableString *)stringMutableCopy;
// [mutableString replaceCharactersInRange:NSMakeRange(1, 2) withString:@"HAHAHA"];
NSLog(@"%@", mutableString);
前面指针的类型不影响后面的对象类型。看一下结果:
2.2 NSMutableString
对于这个可变数据类型的NSMutableString,我进行了一样的验证:
/************ 对于可变对象(NSMutableString)的测试 ************/
NSMutableString *mutableString1 = [NSMutableString stringWithString:@""]; NSMutableString *mutableString2 = mutableString1; // mutableString2与mutableString1地址相同 NSMutableString *mutableStringCopy = [mutableString1 copy]; // mutableStringCopy与mutableString1地址不同 NSMutableString *mutableStringMutableCopy = [mutableString1 mutableCopy]; // mutableStringMutableCopy与mutableString1地址不同
NSLog(@"\n%p\n%p\n%p\n%p", mutableString1, mutableString2, mutableStringCopy, mutableStringMutableCopy); NSLog(@"\n%@\n%@\n%@\n%@", NSStringFromClass([mutableString1 class]), NSStringFromClass([mutableString2 class]), NSStringFromClass([mutableStringCopy class]), NSStringFromClass([mutableStringMutableCopy class]));
与上面做的验证相同。先验证内存地址,在验证对象类型。打印结果如下:
首先可以得出的结论是:对于不可变对象,直接修改指针指向的地址是浅拷贝,拷贝指针;但是copy与mutableCopy操作统统都是深拷贝,拷贝对象与地址。
对于 mutableCopy 后的结果,我想就不用验证了,打印结果很清楚了。但令我非常疑惑的是 copy 得到的 NSTaggedPointerString 是个什么类型呢?不像可变字符串,也不像不可变字符串。查询了一下,发现这还与字符串的长度有关。于是我做了以下修改:
NSMutableString *mutableString1 = [NSMutableString stringWithString:@"aiuhgiulahilhgialjgiuhaioljgiayukugbkuagki"]; NSLog(@"\n%@\n%@\n%@\n%@", mutableString1, mutableString2, mutableStringCopy, mutableStringMutableCopy);
这次够长了吧?来看下打印结果:
这次似乎没有问题。来我们试着执行一下 NSMutableString 特有的方法:
[mutableStringCopy replaceCharactersInRange:NSMakeRange(, ) withString:@"爱咖喱鸡"];
NSLog(@"%@", mutableStringCopy);
崩溃了。
原因是:Attempt to mutate immutable object with replaceCharactersInRange:withString:
翻译成人话:尝试把不可变的对象执行 replaceCharactersInRange:withString: 这个方法。
也就是说,不管打印出来的是什么类型,它始终是一个不可变的对象,不可以执行可变对象特有的方法。
可以得出最终结论了:尽管我们知道在字符串足够长的时候copy出来的对象可以是__NSCFString,但它仍不可以执行NSMutableString的方法。我们可以认为这是个不可变对象NSString。
五.最终结论
1.对于可变与不可变对象:
区别在于是否需要在创建对象的时候确定并固定对象的内存地址的大小与位置。
1.1 不可变对象在初始化之后不能改变自己所储存的内容大小,也就是不可修改自己的内存地址的大小与位置;
1.2 而可变对象则可在初始化之后通过自己的方法修改自己的内存地址的大小和位置。
这就是唯一的区别。
2.对于深拷贝浅拷贝:
区别在于是否对对象拷贝。
2.1 浅拷贝:仅仅是对指向对象的指针的拷贝,先后两个指针指向同一个对象;
2.2 深拷贝:是对对象和指针的双重拷贝,新指针指向新对象,旧指针指向旧对象。
3.对于copy和mutableCopy方法:
3.1 copy:
3.1.1 对于不可变对象,copy相当于做了一次浅拷贝,仅仅拷贝指针,不拷贝对象;
3.1.2 对于可变对象,copy是深拷贝,但是拷贝出来的对象类型为相应的不可变对象。
3.2 mutableCopy
不管是可变还是不可变对象,统统都是深拷贝,返回的统统都是可变对象。