OC-2
2、什么是键-值,键路径是什么
模型的性质是通过一个简单的键(通常是个字符串)来指定的。视图和控制器通过键 来查找相应的属性值。在一个给定的实体中,同一个属性的所有值具有相同的数据类型。键-值编码技术用于进行这样的查找—它是一种间接访问对象属性的机制。
键路径是一个由用点作分隔符的键组成的字符串,用于指定一个连接在一起的对象性质序列。第一个键的 性质是由先前的性质决定的,接下来每个键的值也是相对于其前面的性质。键路径使您可以以独立于模型 实现的方式指定相关 对象的性质。通过键路径,您可以指定对象图中的一个任意深度的路径,使其指向相 关对象的特定属性。
11、atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。
atomic
设置成员变量的@property属性时,默认为atomic,提供多线程安全。
在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。加了atomic,setter函数会变成下面这样:
{lock}
if (property != newValue) {
[property release];
property = [newValue retain];
}
{unlock}
nonatomic
禁止多线程,变量保护,提高性能。
atomic是Objc使用的一种线程保护技术,基本上来讲,是防止在写未完成的时候被另外一个线程读取,造成数据错误。而这种机制是耗费系统资源的,所以在iPhone这种小型设备上,如果没有使用多线程间的通讯编程,那么nonatomic是一个非常好的选择。
指出访问器不是原子操作,而默认地,访问器是原子操作。这也就是说,在多线程环境下,解析的访问器提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。如果你不指定 nonatomic ,在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值。
1、定义属性时,什么情况使用copy、assign、retain?
assign用于简单数据类型,如NSInteger,double,bool, retain和copy用于对象, copy用于当a指向一个对象,b也想指向同样的对象的时候,如果用assign,a如果释放,再调用b会crash,如果用copy 的方式,a和b各自有自己的内存,就可以解决这个问题。
retain 会使计数器加一,也可以解决assign的问题。
另外:atomic和nonatomic用来决定编译器生成的getter和setter是否为原子操作。在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。
加了atomic,setter函数会变成下面这样:
if (property != newValue) {
[property release];
property = [newValue retain];
}
assign
修饰基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char)等等。
此标记说明设置器直接进行赋值,这也是默认值。在使用垃圾收集的应用程序中,如果你要一个属性使用assign,且这个类符合NSCopying协 议,你就要明确指出这个标记,而不是简单地使用默认值,否则的话,你将得到一个编译警告。这再次向编译器说明你确实需要赋值,即使它是可拷贝的。
retain
对其他NSObject和其子类对参数进行release旧值,再retain新值。
指定retain会在赋值时唤醒传入值的retain消息。此属性只能用于Objective-C对象类型,而不能用于Core Foundation对象。(原因很明显,retain会增加对象的引用计数,而基本数据类型或者Core Foundation对象都没有引用计数——译者注)。
注意: 把对象添加到数组中时,引用计数将增加对象的引用次数+1。
copy
对NSString 它指出,在赋值时使用传入值的一份拷贝。拷贝工作由copy方法执行,此属性只对那些实行了NSCopying协议的对象类型有效。更深入的讨论,请参考“复制”部分。
copy与retain:
Copy其实是建立了一个相同的对象,而retain不是:
1.比如一个NSString 对象,地址为0×1111 ,内容为@”STR”,Copy 到另外一个NSString 之后,地址为0×2222 ,内容相同。
2.新的对象retain为1 ,旧有对象没有变化retain 到另外一个NSString 之后,地址相同(建立一个指针,指针拷贝),内容当然相同,这个对象的retain值+1。
总结:retain 是指针拷贝,copy 是内容拷贝。
assign与retain:
- 接触过C,那么假设你用malloc分配了一块内存,并且把它的地址赋值给了指针a,后来你希望指针b也共享这块内存,于是你又把a赋值给(assign)了b。此时a和b指向同一块内存,请问当a不再需要这块内存,能否直接释放它?答案是否定的,因为a并不知道b是否还在使用这块内存,如果a释放了,那么b在使用这块内存的时候会引起程序crash掉。
- 了解到1中assign的问题,那么如何解决?最简单的一个方法就是使用引用计数(reference counting),还是上面的那个例子,我们给那块内存设一个引用计数,当内存被分配并且赋值给a时,引用计数是1。当把a赋值给b时引用计数增加到2。这时如果a不再使用这块内存,它只需要把引用计数减1,表明自己不再拥有这块内存。b不再使用这块内存时也把引用计数减1。当引用计数变为0的时候,代表该内存不再被任何指针所引用,系统可以把它直接释放掉。
总结:上面两点其实就是assign和retain的区别,assign就是直接赋值,从而可能引起1中的问题,当数据为int, float等原生类型时,可以使用assign。retain就如2中所述,使用了引用计数,retain引起引用计数加1, release引起引用计数减1,当引用计数为0时,dealloc函数被调用,内存被回收。
1、宏和const的却别
const简介:之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。
注意:很多Blog都说使用宏,会消耗很多内存,我这验证并不会生成很多内存,宏定义的是常量,常量都放在常量区,只会生成一份内存。
- 编译时刻:宏是预编译(编译之前处理),const是编译阶段。
- 编译检查:宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。
- 宏的好处:宏能定义一些函数,方法。 const不能。
- 宏的坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换。
- 1.const仅仅用来修饰右边的变量(基本数据变量p,指针变量*p)
- 2.被const修饰的变量是只读的。
const:常量
const:当有字符串常量的时候,苹果推荐我们使用const
宏的命名规范:一般以项目前缀开头,key结尾。#开头表编译。
// 宏:替代常用字符串常量
宏的用法:
1、定义常用字符串。
2、定义一段代码。
const与宏的区别:
1、编译时刻:宏-预编译 const-command+b(编译阶段)编译。
2、宏不会检查代码错误,只是替换,但是const会编译报错。
3、宏的好处:定义代码或字符串、方法、参数 const不能。
4、宏坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换。
// 常见的常量:抽成宏
如:#define GENameKey @“123"
const简单使用
const作用:
1.仅仅是用来修饰右边的变量(只能修饰变量:基本变量,指针变量,对象变量)
2.const修饰的变量,表示只读,不能修改
const书写规范:一定要放在变量的左边
/******************** 1、用const修饰基本变量 ********************/
// 定义int只读变量
// 方式一:
int const a = 10; // a:只读变量,不允许修改值
// 方式二:
const int b = 10; // b:只读变量,不允许修改值
/******************** 2、用const修饰指针变量 ********************/
// const:修饰指针变量*p,带*的变量,就是指针变量.
// 定义一个指向int类型的指针变量,指向a的地址
int c = 10;
int d = 20;
int *p = &c;
p = &d;
// 允许修改p指向的地址,
// 允许修改p访问内存空间的值
*p =20;
/*
int * const p = &c; // p:只读变量 *p:变量
const int *p = &c; // *p:只读变量 p:变量
int const *p = &c; // *p:只读 p:变量
int const * const p = &c; // *p:只读 p:只读
const int * const p = &c; // *p:只读 p:只读
*/
// 定义执行a的指针变量
// 修改p的地址
// p = &d;
// *p = 30; // 将d的值改为30
/******************** 3、修饰对象变量 ********************/
NSString * const name = @"123";
// name = @"321";
const开发中使用场景
// 1.定义一个全局只读变量
// 2.在方法中定义只读参数
// 2.1、在方法中定义只读参数:修饰对象
// 2.2、在方法中定义只读参数:修饰基本变量
// 2.3、在方法中定义只读参数:修饰指针变量
static作用:
1.修饰局部变量
*延长这个局部变量的生命周期,只要程序运行,局部变量就会一直存在
*局部变量只会分配一次内存,为什么?用static修饰的代码,只会在程序一启动就会执行,以后就不会在执行
2.修饰全局变量
*只会修改全局变量的作用域,表示只能是当前文件内使用
extern作用: 只声明一个变量,不能定义变量
注意:extern修饰的变量不能初始化
使用场景,一般用于声明全局变量
const作用:限制类型
- 1.const仅仅用来修饰右边的变量(基本数据变量p,指针变量*p)
- 2.被const修饰的变量是只读的。
注意:很多Blog都说使用宏,会消耗很多内存,我这验证并不会生成很多内存,宏定义的是常量,常量都放在常量区,只会生成一份内存。
const的用法:
1、int * const p ; p为只读,*p为变量。
2、const int p ; p为只读,p为变量。
3、int const * const p ;p、p都为只读。
4、const int * const p ;p、p都为只读。
三、const开发中使用场景:
1.需求1:提供一个方法,这个方法的参数是地址,里面只能通过地址读取值,不能通过地址修改值
2.需求2:提供一个方法,这个方法的参数是地址,里面不能修改参数的地址。
const在开发中使用的场景:
1、定义全局只读参数,代替宏,减低编译速度。
2、方法中定义只读参数,不允许更改。
![]()
2、僵尸对象
内存回收的本质.
- 申请1块空间,实际上是向系统申请1块别人不再使用的空间。
- 释放1块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用.
- 在这个空间分配给别人之前 数据还是存在的。
- OC对象释放以后,表示OC对象占用的空间可以分配给别人.
- 但是再分配给别人之前 这个空间仍然存在,对象的数据仍然存在.
- 僵尸对象: 1个已经被释放的对象,就叫做僵尸对象.
使用野指针访问僵尸对象,有的时候会出问题,有的时候不会出问题。
- 当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候,这个时候其实是可以访问的.
- 因为对象的数据还在.
- 当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
- 所以,你不要通过1个野指针去访问1个僵尸对象.
- 虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.
僵尸对象检测
- 默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.
- 可以开启Xcode的僵尸对象检测.
- 那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.
为什么不默认开启僵尸对象检测呢?
- 因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
- 那么这样的话 就影响效率了.
如何避免僵尸对象报错.
当1个指针变为野指针以后. 就把这个指针的值设置为nil
僵尸对象无法复活.
- 当1个对象的引用计数器变为0以后 这个对象就被释放了.
- 就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
- 因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.
一、什么是空指针和野指针
1.空指针
1> 没有存储任何内存地址的指针就称为空指针(NULL指针)
2> 空指针就是被赋值为0的指针,在没有被具体初始化之前,其值为0。
下面两个都是空指针:
Student *s1 = NULL;
Student *s2 = nil;
2.野指针
"野指针"不是NULL指针,是指向"垃圾"内存(不可用内存)的指针。野指针是非常危险的。
二、野指针和空指针例子
接下来用一个简单的例子对比一下野指针和空指针的区别
1.首先,打开Xcode的内存管理调试开关,它能帮助检测垃圾内存
![]()
![]()
![]()
2.自定义Student类,在main函数中添加下列代码
Student *stu = [[Student alloc] init];2
[stu setAge:10];
[stu release];
[stu setAge:10];运行程序,你会发现第7行报错了,是个野指针错误!
![]()
3.接下来分析一下报错原因
1> 执行完第1行代码后,内存中有个指针变量stu,指向了Student对象
Student *stu = [[Student alloc] init];
![]()
假设Student对象的地址为0xff43,指针变量stu的地址为0xee45,stu中存储的是Student对象的地址0xff43。即指针变量stu指向了这个Student对象。
2> 接下来是第3行代码
[stu setAge:10];
这行代码的意思是:给stu所指向的Student对象发送一条setAge:消息,即调用这个Student对象的setAge:方法。目前来说,这个Student对象仍存在于内存中,所以这句代码没有任何问题。
3> 接下来是第5行代码
[stu release];
这行代码的意思是:给stu指向的Student对象发送一条release消息。在这里,Student对象接收到release消息后,会马上被销毁,所占用的内存会被回收。
(release的具体用法会放到OC内存管理中详细讨论)
![]()
Student对象被销毁了,地址为0xff43的内存就变成了"垃圾内存",然而,指针变量stu仍然指向这一块内存,这时候,stu就称为了野指针!
4> 最后执行了第7行代码
[stu setAge:10];
这句代码的意思仍然是: 给stu所指向的Student对象发送一条setAge:消息。但是在执行完第5行代码后,Student对象已经被销毁了,它所占用的内存已经是垃圾内存,如果你还去访问这一块内存,那就会报野指针错误。这块内存已经不可用了,也不属于你了,你还去访问它,肯定是不合法的。所以,这行代码报错了!4.如果改动一下代码,就不会报错
Student *stu = [[Student alloc] init];
[stu setAge:10];
[stu release];
stu = nil;
[stu setAge:10];
注意第7行代码,stu变成了空指针,stu就不再指向任何内存了
![]()
因为stu是个空指针,没有指向任何对象,因此第9行的setAge:消息是发不出去的,不会造成任何影响。当然,肯定也不会报错。
5.总结
1> 利用野指针发消息是很危险的,会报错。也就是说,如果一个对象已经被回收了,就不要再去操作它,不要再尝试给它发消息。
2> 利用空指针发消息是没有任何问题的,也就是说下面的代码是没有错误的:
[nil setAge:10];
6、野指针是什么?iOS开发中什么情况下会出现野指针?
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 野指针:声明一个指针变量,但是没有赋初始值,这个指针变量一块随机的空间,空间的数据就是垃圾值。
// c语言的野指针:当我们声明一个指针变量,但没有为这个指针变量赋初始值,这个指针变量的值就是一个垃圾值,指向一块随机的内存空间。
char *p1;
int i = 10; // 申请空间,保存数据,int占4个自己,执行过后,i被释放
// oc语言的野指针:指针指向的对象已经被回收掉,这个指针就叫野指针。
Person *p2 = [Person new];//1
[p2 hello];
[p2 release];//0,p2的指针还在,这时就是野指针
// p2被释放,变成僵尸对象
// 但是在这个空间分配给别人之前,数据还是存在的。
// 当一个指针变量为野指针以后,就把这个指针的值设置为nil。
p2 = nil;//对空指针调用是不会产生效果,但也不会出问题
// 僵尸对象P2
[p2 hello]; // 访问僵尸对象的数据可能会出问题。我们不建议去访问僵尸对象
// 例如,调用多几次就会出问题
// [p2 hello];
// [p2 hello];
// [p2 hello];
/*
1、内存回收的本质:
申请一块空间,实际上是向系统申请一块别人不再使用的空间。
释放一块空间,指的是占用的空间不再使用,这个时候系统就可以分配给别人去使用。
但是在这个空间分配给别人之前,数据还是存在的。
oc对象释放以后,表示oc对象占用的空间可以分配给别人。
但是在分配给别人之前,这个空间仍然存在,对象的数据也仍然存在。
2、僵尸对象:一个已经被释放的对象,就叫僵尸对象。
3、使用野指针访问僵尸对象,有的时候会出现问题,有的时候不会出问题。
当野指针指向的将死对象所占用的空间还没有分配给别人的时候,这个时候其实是可以范根的。
因为对象的数据还在。
当也指责你执行的对象所占的空间分配给了别人的时候,这个时候访问就会出问题。
所以,你不要通过一个野指针去访问一个僵尸对象。
4、僵尸对象检测
默认情况下,xcode不会去检测指针指向的对象是否为一个僵尸对象,能访问就访问,不能访问就报错
可以开启xcode的僵尸对象检测。
那么就会在通过指针访问对象的时候,检测这个对象是否为一个僵尸对象,如果是,就会报错。
5、为什么不默认开启僵尸对象检测?
因为一旦开启,每次通过指针访问对象的时候,都会去检测指针指向的对象是否为僵尸对象,这样会影响效率。
6、如何避免僵尸对象报错?
当一个指针变量为野指针以后,就把这个指针的值设置为nil。
*/
// 打开僵尸对象检测功能
// Edit Scheme -> Run -> Diagnostics -> Memory Management -> Zombie Objects
/*
僵尸对象无法复活:
当一个对象的引用计数器变为0后,这个对象就被释放了。
就无法去操作这个僵尸对象了,所以对这个对象的操作都是无效的。
因为一旦对象被回收,对象就是一个僵尸对象,而访问一个僵尸对象是没有意义的。
*/
// [p2 retain];//1?
/*
如何避免僵尸对象:
1、开启僵尸对象检测功能
2、当一个指针变量为野指针以后,就把这个指针的值设置为nil。
*/
}
return 0;
}
* 野指针.
* C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾值,指向1块随机的内存空间。
* OC语言: 指针指向的对象已经被回收掉了,这个指针就叫做野指针.
9、ARC可能出现野指针吗?a.有的话出现的场景是怎样的?b.没有的话是因为什么原因呢?
1.ARC小结
1.有了ARC,我们的代码可以清晰很多,你不再需要考虑什么时候retain或release对象。唯一需要考虑的是对象之间的关联,也就是哪个对象拥有哪个对象?
2.ARC也有一些限制:
1> 首先ARC只能工作于Objective-C对象,如果应用使用了Core Foundation或malloc()/free(),此时还是需要你来手动管理内存
2> 此外ARC还有其它一些更为严格的语言规则,以确保ARC能够正常地工作
3.虽然ARC管理了retain和release,但并不表示你完全不需要关心内存管理的问题。因为strong指针会保持对象的生命,某些情况下你仍然需要手动设置这些指针为nil,否则可能导致应用内存不足。无论何时你创建一个新对象时,都需要考虑谁拥有该对象,以及这个对象需要存活多久。
4.ARC还能很好地结合C++使用,这对游戏开发是非常有帮助的。对于iOS 4,ARC有一点点限制(不支持weak指针),但也没太大关系。
2.背景:
在自定义TableViewCell类中:
点击cell上的某个按钮,弹出AlertView。
点击事件中,初始化这个alertView,调用alertView自定义初始化方法(initWithFrame:delegate:self),传递self即Cell,设置代理。
在AlertView类中:
声明协议,并有三个协议方法如rightBtnClicked:等
使用前设置代理,在alertView,alloc的时候,设置的代理self.delegate = xxxCell
点击如右边的button,触发事件
-(void)btnClicked:(UIButton *)btn {
[self.delegate rightBtnClicked:btn] // crash,此时delegate已经被释放了
}
实际上就是cell对象已经被释放,即delegate被dealloc了。指针没有置空,这时再访问这个指针指向的内存,就会 Crash。
无法用if (nil = delegate)判断delegate是否已经被dealloc掉,因为被dealloc之后,delegate对象也不是空的,这个时候delegate可能已经是野指针了。
3.解决办法。
可以在cellforrow里判断,在delegate是被释放了的情况,重新赋值,设置代理。
调用者在对象销毁时未将delegate置为nil,delegate将变成野指针,而导致程序挂掉。设置nil。
weak,assign。我们的delegate,在arc中用weak
今天在公司爆出一个 BUG,导致5000+crash.
大致是 UIKit 中的 delegate 访问了已经释放了的界面,也就是使用了野指针导致 crash.
回来演示了一下发现
@property (nonatomic, assign) id<MyCellDelegate> delegate;//1
@property (nonatomic, weak) id<MyCellDelegate> delegate;//2
大部分的 UIKit 的 delegate 都是如1的声明
因为 ios 在5之前是没有 ARC 的,为了兼容所以写的都是 assign
那么 assign 与 weak 有什么区别呢?
__strong NSString *yourString = [[NSString alloc] initWithUTF8String:"your string"];
__weak NSString *myString = yourString;
yourString = nil;
__unsafe_unretained NSString *theirString = myString;
//现在所有的指针都为nil
weak的特性,如果指向的内存被释放了,则自动指向 nil;
所以使用 weak 是不会有野指针的
而 assign 和unsafe_unretained,永远指向某内存地址,如果该内存被释放了,自己就会成为野指针
如下
__strong NSString *yourString = @"Your String";
__weak NSString *myString = yourString;
__unsafe_unretained NSString *theirString = myString;
yourString = nil;
//现在yourString与myString的指针都为nil,而theirString不为nil,但是是野指针。
所以我们在使用 UIKit 中的 delegate 的时候,要避免响应 delegate 的VC,或者 View 之类的实例被提前释放了,而导致 crash
而我们自己的 delegate 可以直接写成 weak 的,既避免了循环引用,又不会产生野指针.
PS:assign 只能用在属性的定义,变量的定义就可以用类似的 unsafe_unretained
13、写一段反转单词的代码,并将时间复杂度用大O表达。例如将”IAM HERO”转换成“I MA OREH”。可以用伪代码。
// 反转单词
-(void)inversionWord {
NSString * str1 = @"I AM HERO";
NSArray *arr = [str1 componentsSeparatedByString:@" "];
NSString *mStr = [NSString string];
for (int i = 0; i < arr.count; ++i) {
NSString *str2 = arr[i];
NSMutableString *reverseString = [NSMutableString string];
for(int i = 0 ; i < str2.length; i ++){
//倒序读取字符并且存到可变数组数组中
unichar c = [str2 characterAtIndex:str2.length- i -1];
[reverseString appendFormat:@"%c",c];
}
mStr = [mStr stringByAppendingString:[NSString stringWithFormat:@" %@",reverseString]];
NSLog(@"%@",mStr);
}
}
2、对于语句 NSString *obj = [[NSData alloc] init];obj在编译时和运行时分别是什么类型的对象?
编译时是NSString的类型;
运行时是NSData类型的对象。
1、什么情况下使用weak关键字,相比assign有什么不同?
什么情况使用 weak 关键字?
- 在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。
- 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak。
不同点:
- weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。而 assign的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。
- assigin 可以用非 OC 对象,而 weak 必须用于 OC 对象
11、说说你对程序编译过程(从源代码到可执行文件)的理解,有哪些步骤?(提示:预处理,汇编等)
一 以下是C程序一般的编译过程:
![]()
从图中看到:
将编写的一个c程序(源代码 )转换成可以在硬件上运行的程序(可执行代码 ),需要进行编译阶段 和链接这两个阶段。其中,
编译阶段先通过“编译器 “把一个 .c / .cpp 源代码 编译成 .s的汇编代码 ;再经过“汇编器 ” 把这 个.s的汇编代码汇编成 .o 的 目标代码
“连接器 “ 通过连接其他 .o 代码(如果需要的话) 库文件 和 1 中的.o 目标代码生成可执行文件
该文件流被这三种程序(红色)的加工,分别表现出四种形式(蓝色) ,这就是c程序的编译和链接过程。如果再详细的话,编译器在将源文件编译成汇编文件的过程又分为:预处理阶段(生成 .i代码) 和 优化阶段
二、C编程中的文件后缀名介绍
.c 未经过预处理的C源码
.h C头文件
.i 经过预处理的C源码
.s 生成的汇编语言代码
.o 编译之后产生的目标文件
解释:
*.c一般使我们自己编辑的代码,使我们劳动的结晶;
.h一般是我们手工生成的接口文件,如果愿意,也可在.c完成后用GCC的选项-aux-info帮我们生成;
*.i是经过预处理后的源码,是由GCC在选项-E编译下自动生成的文件;
*.o是编译后产生的目标文件;
*.s是GCC在选项-S编译下生成的汇编语言代码,对于性能要求很高的程序可以先生成汇编语言文件并对汇编做优化,然后用优化后的汇编生成目标文件并链接
C语言编译过程详解
前言
C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。
编写hello world C程序:
// hello.c #include <stdio.h> int main(){ printf("hello world!\n"); }
编译过程只需:
$ gcc hello.c # 编译
$ ./a.out # 执行
hello world!这个过程如此熟悉,以至于大家觉得编译事件很简单的事。事实真的如此吗?我们来细看一下C语言的编译过程到底是怎样的。
上述gcc命令其实依次执行了四步操作:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.链接(Linking)。
示例
为了下面步骤讲解的方便,我们需要一个稍微复杂一点的例子。假设我们自己定义了一个头文件mymath.h,实现一些自己的数学函数,并把具体实现放在mymath.c当中。然后写一个test.c程序使用这些函数。程序目录结构如下:
├── test.c
└── inc
├── mymath.h
└── mymath.c
程序代码如下:
// test.c
#include <stdio.h>
#include "mymath.h"// 自定义头文件
int main(){
int a = 2;
int b = 3;
int sum = add(a, b);
printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
}
头文件定义:
// mymath.h
#ifndef MYMATH_H
#define MYMATH_H
int add(int a, int b);
int sum(int a, int b);
#endif
头文件实现:
// mymath.c
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
1.预处理(Preprocessing)
预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的,你可以通过如下命令对test.c进行预处理:
gcc -E -I./inc test.c -o test.i
或者直接调用cpp命令
$ cpp test.c -I./inc -o test.i上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。
经过预处理之后代码体积会大很多:
![]()
预处理之后的程序还是文本,可以用文本编辑器打开。
2.编译(Compilation)
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。编译的指定如下:
$ gcc -S -I./inc test.c -o test.s
上述命令中-S让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码test.s,这也是文本文件,内容如下:
3.汇编(Assemble)
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc汇编过程通过as命令完成:
$ as test.s -o test.o
等价于:
gcc -c test.s -o test.o
这一步会为每一个源文件产生一个目标文件。因此mymath.c也需要产生一个mymath.o文件
4.链接(Linking)
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
命令大致如下:
$ ld -o test.out test.o inc/mymath.o ...libraries...
结语
经过以上分析,我们发现编译过程并不像想象的那么简单,而是要经过预处理、编译、汇编、链接。尽管我们平时使用gcc命令的时候没有关心中间结果,但每次程序的编译都少不了这几个步骤。也不用为上述繁琐过程而烦恼,因为你仍然可以:
$ gcc hello.c # 编译
$ ./a.out # 执行
C语言编译过程简介
刚开始接触编程的时候,只知道照书敲敲代码,一直都不知道为什么在windows平台下代码经过鼠标那样点击几下,程序的结果就会在那个黑色的屏幕上。现在找了个机会将C语言的编译原理做一下小小的总结,这样也能为以后我们进军Linux编程做一些准备工作,现在这里和大家一起分享分享。O(∩_∩)O~
讲到编译原理,我觉得首先我们得明白一些基本概念。
1.编辑器:我们编写代码的一些窗口,如:记事本、word、notepad 等。
2.编译器:检查用户代码的一些语法错误并且将其编译成汇编代码。
3.汇编器:将编译出来的文件变成目标代码(windows 下的.obj文件)
4.连接器:将目标代码连接成为可执行文件(.exe),及双击就可以运行文件。
5.集成开发环境(Integrated Development Environment,简称IDE):是用于程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面工具。如:VC6.0、C_Free等。
好了,下面大家来看看整个过程吧:如图片
Ok!现在是不是对程序的编译有一点概念了,O(∩_∩)O~。现在稍微总结一下编译的完整过程吧:
C源程序-->预编译处理(.c)-->编译、优化程序(.s、.asm)-->汇编程序(.obj、.o、.a、.ko)-->链接程序(.exe、.elf、.axf等)。现在我们就来逐条了解一下每个过程吧。
1.C源程序
这个大家都清楚了,那就是自己编写程序代码。
2.预编译处理(.c)
它主要包括四个过程
a、宏定义指令,如#define N 6,#undef等。
对于前一个伪指令,预编译所要做的是将程序中的所有N用6替换,请大家注意这里是替换,并不是像作为函数参数那样将6复制进N这个变量。对于后者,则将取消对某个宏的定义,使以后出现的N不再被替换。
b、条件编译指令,如#ifdef,#ifndef,#endif等。
这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。这样就能在编译阶段减少编译时间,提高效率,看看这是多好的指令。O(∩_∩)O~
c、 头文件包含指令,如#include "file.h"或#include <file.h>等。
在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。
采用这样的做法一来可以让我们直接调用一些复杂库函数;二来可以免去我们在写程序时重复做一些定义声明工作的麻烦。试想一下,一旦我们写好头文件,那么以后要用到相关模块就再也不用写这些函数了,直接#include 就OK了,这可是一劳永逸啊,天大的便宜呢,呵呵。
对了,这里顺便提一下#include<>与#include“”的区别。
.#include<>:这条指令就是告诉编译器去系统默认的路径寻找相关文件。
.#include”” :这条是告诉编译器先去源程序所在目录下寻找,如果没有就去系统默认路径寻找。
d、特殊符号,预编译程序可以识别一些特殊的符号。
例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序就是对在源程序中出现的这些特殊符号将用合适的值进行替换。大家注意到没,预编译阶段基本上是完成对源程序的相关代码进行替换,这样之后程序的原意没有改变,就是代码的内容有所不同,这样为以后的编译做好准备,ok,第二阶段完满结束。
3.编译、优化程序(.s、.asm)
经过上一阶段的处理,现在我们的程序已经没有宏定义,包含头文件等指令了,只剩下一些变量,常量,关键字等,而编译的主要作用是检查这些代码的语法错误及将这些代码编译成为汇编文件。
优化程序这是很复杂的,不仅和编译技术本身有关,还和目标板相应的硬件环境有很大的关系。如下面的代码:
int a,b,c;
a = inWord(0x100); /读取 I/O 空间 0x100 端口的内容存入 a 变量/
b = a;
a = inWord (0x100); /再次读取 I/O 空间0x100端口的内容存入 a 变量/
c = a;
很可能被编译器优化为:
int a,b,c;
a = inWord(0x100); /读取 I/O 空间 0x100 端口的内容存入 a 变量/
b = a;
c = a;
也正是由于这种编译器优化作用才使关键字volatile有了这么大的用武之地,当然这只是原因之一。4.汇编程序(.obj、.o、.a、.ko)
在这个阶段是将汇编代码翻译成目标文件,这时的文件已经是二进制代码了。在windows环境下文件的后缀名是.obj;而在unix下则有是o、.a、.ko等文件。
目标文件由段组成。通常一个目标文件中至少有两个段:
代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。5.链接程序(.exe、.elf、.axf)
也许有人会有疑问,上面的目标代码已经是机器码了,也就是说CPU可以识别这些文件了,那为什么我们还要链接程序呢?大家想想我们是不是忘了点什么。。。对!那些被包含的头文件,以及当我们的程序分布于很多源文件时,那么这些源文件该怎么处理呢,这就是连接器的作用,它们被翻译成目标代码后需要被链接到一起才能被执行。这样就ok了,嘿嘿!谈到函数库的链接,我们还需要了解点函数库的知识,函数库分静态链接库(又称静态库.lib)和链接动态库(又称动态库.dll)。
静态库的链接在编译时会被编译进汇编文件,这样的操作会改变文件大小;而动态库则是在执行时(双击运行),当需要动态库中的文件时才被链接到可执行文件的。
Ok!总结就到这里吧,希望对大家能有所帮助,O(∩_∩)O~~~~
2、Objective-C中NSString的isEquel、isEquelToString、==区别。
isEqual:首先判断两个对象是否类型一致, 再判断具体内容是否一致,如果类型不同直接return no.如先判断是否都是 NSString,再判断string的内容。
isEqualToString::这个直接判断字符串内容,当然你要确保比较的对象保证是字符串。
==:应该是直接比较指向的地址吧(貌似apple没有说过推荐使用==来进行object的比较吧)首先 OC中的对象都是用指针表示,方法的调用是基于消息机制实现,== 比较的自然是指针指向的地址
isEqual 和 isEqualToString 的区别
IsEqual 是 NSObject 的方法 ,而 isEqualToString 是 NSString 的方法
因此从继承关系角度来说isEqualToString 是 isEqual 的衍生方法简单说一下:
首先都会判断指针是否相等 ,相等直接返回YES,
不相等再判断是否是同类对象或非空,空或非同类对象直接返回NO,
而后依次判断对象对应的属性是否相等,若均相等,返回YES
这样就不难理解 isEqualToString 的实现内部的了最后解释 HashCode 和 isEqual 的关系
hash和isEqual:方法都在NSObject协议中声明,且彼此关系紧密。实现hash方法必须返回一个整型数(NSInterger),作为哈希表结构中的表地址。
两个对象相等(isEqual:方法的判断结果)意味着它们有相同的哈希值。如果哈希值相同,两个对象不一定相等。
如果您的对象可能被包含在象NSSet这样的集合中,则需要定义hash方法,比如UIWebView
并确保该方法在两个对象相等的时候返回相同的哈希值。
5、什么时候使用NSMutableArray,什么时候使用NSArray?
当数组在程序运行时,需要不断变化的,使用NSMutableArray,当数组在初始化后,便不再改变的,使用NSArray。
需要指出的是,使用NSArray只表明的是该数组在运行时不发生改变,即不能往NSAarry的数组里新增和删除元素。但不表明其数组内的元素的内容不能发生改变。NSArray是线程安全的,NSMutableArray不是线程安全的,多线程使用到NSMutableArray需要注意。
10、说说面向对象的三大特征,说说你对Program to an interface,not to an implementation这句OO里面的核心原则的理解。
面向对象的三大特征:封装,继承,多态
封装:
就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
继承:
指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过 “继承”(Inheritance)和“组合”(Composition)来实现。
继承概念的实现方式有二类:实现继承与接口继承。
实现继承是指直接使用 基类的属性和方法而无需额外编码的能力;
接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
多态:
是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
面向对象的五大基本原则
单一职责原则(SRP)
开放封闭原则(OCP)
里氏替换原则(LSP)
依赖倒置原则(DIP)
接口隔离原则(ISP)
1、单一职责原则(SRP)
* 一个类应该仅有一个引起它变化的原因(最简单,最容易理解却最不容易做到的一个设计原则)
职员类例子:
比如在职员类里,将工程师、销售人员、销售经理这些情况都放在职员类里考虑,其结果将会非常混乱,在这个假设下,职员类里的每个方法都要if else判断是哪种情况,从类结构上来说将会十分臃肿,并且上述三种的职员类型,不论哪一种发生需求变化,都会改变职员类!这个是大家所不愿意看到的!
2、开放封闭原则(OCP)
* 既开放又封闭,对扩展是开放的,对更改是封闭的!
* 扩展即扩展现行的模块,当我们软件的实际应用发生改变时,出现新的需求,就需要我们对模块进行扩展,使其能够满足新的需求!
更改封闭即是在我们对模块进行扩展时,勿需对源有程序代码和DLL进行修改或重新编译文件!
这个原则对我们在设计类的时候很有帮助,坚持这个原则就必须尽量考虑接口封装,抽象机制和多态技术!
3、里氏替换原则(LSP)
* 子类可以替换父类并且出现在父类能够出现的任何地方
* 这个原则也是在贯彻GOF倡导的面向接口编程!
在这个原则中父类应尽可能使用接口或者抽象类来实现!
子类通过实现了父类接口,能够替父类的使用地方!
通过这个原则,我们客户端在使用父类接口的时候,通过子类实现!
意思就是说我们依赖父类接口,在客户端声明一个父类接口,通过其子类来实现
这个时候就要求子类必须能够替换父类所出现的任何地方,这样做的好处就是,在根据新要求扩展父类接口的新子类的时候而不影响当前客户端的使用!
4、依赖倒置原则(DIP)
* 传统的结构化编程中,最上层的模块通常都要依赖下面的子模块来实现,也
称为高层依赖低层!
所以DIP原则就是要逆转这种依赖关系,让高层模块不要依赖低层模块,所以称之为依赖倒置原则!
5、ISP 接口隔离原则
* 这个原则的意思是:使用多个专门的接口比使用单个接口要好的多!
这个我有体会,在我实际编程中,为了减少接口的定义,将许多类似的方法都放在一个接口中,最后发现,维护和实现接口的时候花了太多精力,而接口所定义的操作相当于对客户端的一种承诺,这种承诺当然是越少越好,越精练越好,过多的承诺带来的就是你的大量精力和时间去维护!
Program to an interface,not to an implementation:针对接口编程,而非(接口的)实现
* 接口
1.接口是一个对象在对其它的对象进行调用时所知道的方法集合。
2.一个对象可以有多个接口(实际上,接口是对象所有方法的一个子集)
3.类型是对象的一个特定的接口。
4.不同的对象可以具有相同的类型,而且一个对象可以具有多个不同的类型。
5.一个对象仅能通过其接口才会被其它对象所了解。
6.某种意义上,接口是以一种非常局限的方式,将"是一种…"表达为"一种支持该接口的…"。
7.接口是实现插件化(pluggability)的关键
* 实现继承和接口继承
1.实现继承(类继承):一个对象的实现是根据另一个对象的实现来定义的。
2.接口继承(子类型化):描述了一个对象可在什么时候被用来替代另一个对象。
3.C++的继承机制既指类继承,又指接口继承。
4.C++通过继承纯虚类来实现接口继承。
5.Java对接口继承具有单独的语言构造方式-Java接口。
6.Java接口构造方式更加易于表达和实现那些专注于对象接口的设计。
* 接口的好处
1.优点:
a.Client不必知道其使用对象的具体所属类。
b.一个对象可以很容易地被(实现了相同接口的)的另一个对象所替换。
c.对象间的连接不必硬绑定(hardwire)到一个具体类的对象上,因此增加了灵活性。
e.松散藕合(loosens coupling)。
f.增加了重用的可能性。
e.提高了(对象)组合的机率,因为被包含对象可以是任何实现了一个指定接口的类。
2.缺点:
a.设计的复杂性略有增加
(译者注:接口表示"…像…"(LikeA)的关系,继承表示"…是…"(IsA)的关系,组合表示"…有…"(HasA)的关系。)
面向对象的三大特征:封装,继承,多态。
面向对象三大特性,五大原则
以前一直认为程序中的类有使用到封装继承多态就是面向对象设计,其实不然,封装、继承、多态只是面向对象的三大特性,但是在设计程序的时候并不是说类的结构使用到了(或是体现出了)这三个特性就是面向对象,其实真正的面向对象设计是要符合下面的五大原则。
面向对象的五大基本原则
1、单一职责原则(SRP)
2、开放封闭原则(OCP)
3、里氏替换原则(LSP)
4、依赖倒置原则(DIP)
5、接口隔离原则(ISP)
1、单一职责原则(SRP)
• 一个类应该仅有一个引起它变化的原因(最简单,最容易理解却最不容易做到的一个设计原则)
职员类例子:
比如在职员类里,将工程师、销售人员、销售经理这些情况都放在职员类里考虑,其结果将会非常混乱,在这个假设下,职员类里的每个方法都要if else判断是哪种情况,从类结构上来说将会十分臃肿,并且上述三种的职员类型,不论哪一种发生需求变化,都会改变职员类!这个是大家所不愿意看到的!
2、开放封闭原则(OCP)
• 既开放又封闭,对扩展是开放的,对更改是封闭的!
• 扩展即扩展现行的模块,当我们软件的实际应用发生改变时,出现新的需求,就需要我们对模块进行扩展,使其能够满足新的需求!
更改封闭即是在我们对模块进行扩展时,勿需对源有程序代码和DLL进行修改或重新编译文件!
这个原则对我们在设计类的时候很有帮助,坚持这个原则就必须尽量考虑接口封装,抽象机制和多态技术!
3、里氏替换原则(LSP)
• 子类可以替换父类并且出现在父类能够出现的任何地方
• 这个原则也是在贯彻GOF倡导的面向接口编程!
在这个原则中父类应尽可能使用接口或者抽象类来实现!
子类通过实现了父类接口,能够替父类的使用地方!
通过这个原则,我们客户端在使用父类接口的时候,通过子类实现!
意思就是说我们依赖父类接口,在客户端声明一个父类接口,通过其子类来实现
这个时候就要求子类必须能够替换父类所出现的任何地方,这样做的好处就是,在根据新要求扩展父类接口的新子类的时候而不影响当前客户端的使用!
4、依赖倒置原则(DIP)
• 传统的结构化编程中,最上层的模块通常都要依赖下面的子模块来实现,也
称为高层依赖低层!
所以DIP原则就是要逆转这种依赖关系,让高层模块不要依赖低层模块,所以称之为依赖倒置原则!
5、ISP 接口隔离原则
• 这个原则的意思是:使用多个专门的接口比使用单个接口要好的多!
这个我有体会,在我实际编程中,为了减少接口的定义,将许多类似的方法都放在一个接口中,最后发现,维护和实现接口的时候花了太多精力,而接口所定义的操作相当于对客户端的一种承诺,这种承诺当然是越少越好,越精练越好,过多的承诺带来的就是你的大量精力和时间去维护!
透切理解面向对象三大基本特性是理解面向对象五大基本原则的基础
三大基本特性:封装,继承,多态
1、封装:
就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
2、继承:
指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过 “继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用 基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
3、多态:
是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
12、将16进制(@“#fcdf39”)转成UIColor,请简单写一个实现。
+(UIColor *)getColor:(NSString *)hexColor {
unsigned int red,green,blue;
NSRange range;
range.length = 2;
range.location = 0;
[[NSScanner scannerWithString:[hexColor substringWithRange:range]] scanHexInt:&red];
range.location = 2;
[[NSScanner scannerWithString:[hexColor substringWithRange:range]] scanHexInt:&green];
range.location = 4;
[[NSScanner scannerWithString:[hexColor substringWithRange:range]] scanHexInt:&blue];
return [UIColor colorWithRed:(float)(red/255.0f) green:(float)(green / 255.0f) blue:(float)(blue / 255.0f) alpha:1.0f];
}
6、readwrite,readonly,assign,retain,copy,nonatomic属性的作用
答:
readwrite 是可读可写特性,需要生成getter方法和setter方法时
readonly 是只读特性 ,只会生成getter方法,不会生成setter方法
assign 是赋值特性,setter方法将传入参数赋值给实例变量
retain 表示持有特性,setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1
copy 表示拷贝特性,setter方法将传入对象复制一份
nonatomic 非原子操作,决定编译器生成的setter、getter是否是原子操作
atomic表示多线程安全,一般使用nonatomic
7、__block在arc和非arc下的含义一样吗?
答:
在非arc中,block修饰的变量的引用计算是不变的。
在arc中,会引用到,并且计算+1;
1、以下程序的运行结果是:
int m = 6;
if (m++>6) { // m++先用后加,此时m=6,所以走else
NSLog(@"%d",m);
} else {
NSLog(@"%d",--m); // m=7,--m=6,--m:先减后用,所以打印结果为6
}
运行结果:6
2、在一个对象的方法里面:self.name = “object”;和name=“object”;有什么不同吗?
self.name = @"object"会调用对象的setName()方法,
name = @ "object" 会直接把object赋值给当前对象的name 属性。
并且 self.name 这样retainCount会加1,而name就不会。
3、这段代码有什么问题吗?
@implementation Person
-(void)setAge:(int)newAge {
self.age = newAge;
}
@end
答:
会死循环,会重复调用自己!self.age 改为_age即可;
并且书写不规范:setter方法中的newAge应该为age
- 为什么 NSString 使用 copy 修饰更安全
答:
首先,为什么要用copy?
因为copy安全!
copy修饰的NSString,在初始化时,如果来源是NSMutableString的话,会对来源进行一次深拷贝,将来源的内存地址复制一份,这样,两个对象就一点关系就没有了,无论你怎么操作来源,都不会对自己的NSString有任何影响。
比如:
你有一个@property(nonatomic,copy) NSString *str;
然后有一个NSMutableString *sourceStr;
当你进行str = sourceStr操作之后,紧接着你又改变了sourceStr的内容sourceStr = @"change";那么str的内容并不会改变. 如果你的str不是copy修饰的,而是strong修饰的,那么str的值也会变成@"change";因为strong是浅拷贝的,并不会对来源的内存地址进行拷贝。那么问题来了,既然copy安全,那为什么不都用copy?
这里我们需要了解一点,copy修饰的NSString在进行set操作时,底层是这样实现的:
我们还是举上面那个例子,进行str = sourceStr操作时,内部会执行一个操作:
str = [sourceStr copy];那么这个copy里面做了什么呢?
if ([str isMemberOfClass:[str class]])
没错,就是进行一次判断,判断来源是可变的还是不可变的,如果是不可变,那么好,接下来的操作就跟strong修饰的没有区别,进行浅拷贝;如果是可变的,那么会进行一次深拷贝。
所以,copy操作内部会进行判断,你别小看了这个if操作所消耗的内存,一次不重要,十次可能也可以忽略不计,但当你的项目十分庞大时,有成百上千个个NSString对象,多多少少会对你的app的性能造成一定的影响.那么回到最初的问题,什么时候用copy,什么时候用strong
你只需要记住一点,当你给你的的NSString对象赋值时,如果来源是NSMutableString,那么这种情况就必须要用copy;如果你确定来源是不可变类型的,比如@"http://www.jianshu.com/users/691d9ed740cf/latest_articles"这种固定的字符串,那么用strong比较好