目录
继承的本质
结构体
类的本质
SEL方法
点语法
property(重点,阐述了属性写法的发展过程)
synthesize
property增强
动态类型和静态类型
动态类型检测
id类型
构造方法
继承的本质
- 创建1个对象,这个对象在内存中是如何分配的.
1). 子类对象中有自己的属性和所有父类的属性.
2). 代码段中的每1个类都有1个叫做isa的指针,这个指针指向它的父类.
一直指到NSObject
[p1 sayHi]; //假设p1是Person对象.
先根据p1指针找到p1指向的对象,然后根据对象的isa指针找到Person类.
搜索Person类中是否有这个sayHi方法 如果有执行
如果没有 就根据类的isa指针找父类 。。。。。
NSObject 中如果没有就报错. 如图所示:
结构体
-
结构体与类的相同点
都可以将多个数据封装为1个整体.
struct Date { int year; int month; int day; }; @interface Date : NSObject { int year; int month; int day; } @end
-
结构体与类的不同点
1). 结构体只能封装数据,而类不仅可以封装数据还可以封装行为.
2). 结构体变量分配在栈空间 (如果是1个局部变量的情况下)
而对象分配在堆空间.
栈的特点: 空间相对较小. 但是存储在栈中的数据访问的效率更高一些.
堆的特点: 空间相对较大. 但是存储在堆中的数据访问的效率相对要低3). 赋值.
结构体 Student
类: PersonStudent s1 = {"jack",19,GenderMale};
Student s2 = s1;Person *p1 = [Person new];
Person *p2 = p1; -
应用场景
1). 如果表示的这个实体 不仅是由多个数据组成, 这个是实体还有行为,只能使用类.
2). 如果表示的实体没有行为.光有属性.
-
如果属性较少.只有几个. 那么这个时候就定义为结构体 分配在栈 提高效率.
-
如果属性较多.不要定义成结构体. 因为这样结构体变量会在栈中占据很大1块空间
反而会影响效率. 那就定义为类.
-
类的本质
-
内存中的五大区域.
栈
堆
BSS段
数据段
代码段.代码段:是用来存储代码的.
类加载. 当类第1次被访问的时候 这个类就会被加载到代码段存储起来.
-
讨论三个问题
1). 类什么时候加载到代码段.
类第1次被访问的时候,类就会被加载到代码段存储 类加载.2). 类以什么样的形式存储在代码段.
3). 类一旦被加载到代码段之后 什么时候回收.
是不会被回收的 除非程序结束. -
类是以什么样的形式存储在代码段的.
-
任何存储在内存中的数据都有1个数据类型.
int num = 12; float 12.12f 'a'
任何在内存中申请的空间也有自己的类型.
Person *p1 = [Person new];
-
在代码段存储类的那块空间是个什么类型的.
在代码段中存储类的步骤
a. 先在代码段中创建1个Class对象, Class是Foundation框架中的1个类.
这个Class对象就是用来存储类信息的.b. 将类的信息存储在这个Class对象之中.
这个Class对象.至少有3个属性
类名: 存储的这个类的名称.
属性s: 存储的这个类具有哪些属性
方法s: 存储的这个类具有哪些方法.所以.类是以Class对象的形式存储在代码段的.
存储类的这个Class对象 我们也叫做类对象. 用来存储类的1个对象.所以,存储类的类对象也有1个叫做isa指针的属性 这个指针指向存储父类的类对象.
-
如何拿到存储在代码段中的类对象.
1). 调用类的类方法 class 就可以得到存储类的类对象的地址.
2). 调用对象的对象方法 class 就可以得到存储这个对象所属的类的Class对象的地址.
3). 对象中的isa指针的值其实就是代码段中存储类的类对象的地址.
注意:
声明Class指针的时候 不需要加* 因为在typedef的时候已经加了*了.
-
如何使用类对象.
-
拿到存储类的类对象以后.
Class c1 = [Person class];
c1对象就是Person类.
c1 完全等价于 Person -
使用类对象来调用类的类方法.
因为类对象就代表存储在这个类对象中的类.
Class c1 = [Person class];
c1就代表Person类.所以在使用Person的地方完全可以使用c1代替.
比如我们使用类名来调用类方法.
[Person sayHi];
完全可以使用c1来调用. 因为c1就是Person
[c1 sayHi];
-
可以使用类对象来调用new方法 创建存储在类对象中的类的对象.
Person *p1 = [Person new];
Class c1 = [Person class];
其实创建Person对象 也可以这么做.Person *p2 = [c1 new];
-
注意:
使用类对象 只能调用类的类方法,因为类对象就等价于存在其中的类.
Class c1 = [Person class];
c1就是Person。
-
类是以Class对象的形式存储在代码段之中的.
如何拿到存储类的类对象.
有神马用?
可以使用类对象调用类的类方法.
Class c1 = [Person class];
要调用Person的类方法 可以使用Person去调用.
也可以使用c1去调用.
SEL方法
SEL 全称叫做 selector 选择器.
SEL 是1个数据类型. 所以要在内存中申请空间存储数据.
SEL其实是1个类. SEL对象是用来存储1个方法的.-
类是以Class对象的形式存储在代码段之中.
类名:存储的这个类的类名. NSString
还要将方法存储在类对象之中.如何将方法存储在类对象之中.
1). 先创建1个SEL对象.
2). 将方法的信息存储在这个SEL对象之中.
3). 再将这个SEL对象作为类对象的属性. -
拿到存储方法的SEL对象.
1). 因为SEL是1个typedef类型的 在自定义的时候已经加了.
所以 我们在声明SEL指针的时候 不需要加2). 取到存储方法的SEL对象,
SEL s1 = @selector(方法名); -
调用方法的本质.
[p1 sayHi];
内部的原理:
1). 先拿到存储sayHi方法的SEL对象,也就是拿到存储sayHi方法的SEL数据. SEL消息.2). 将这个SEL消息发送给p1对象.
3). 这个时候,p1对象接收到这个SEL消息以后 就知道要调用方法
4). 根据对象的isa指针找到存储类的类对象.
5). 找到这个类对象以后 在这个类对象中去搜寻是否有和传入的SEL数据相匹配的.
如果有 就执行 如果没有再找父类 直到NSObjectOC最重要的1个机制:消息机制.
调用方法的本质其实就是为对象发送SEL消息.
[p1 sayHi]; 为p1对象发送1条sayHi消息.
重点掌握:
1).方法是以SEL对象的形式存储起来.
2).如何拿到存储方法的SEL对象.-
手动的为对象发送SEL消息.
1). 先得到方法的SEL数据.
2). 将这个SEL消息发送给p1对象.
调用对象的方法 将SEL数据发送给对象.
-(id)performSelector:(SEL)aSelector; Person *p1 = [Person new]; SEL s1 = @selector(sayHi); [p1 performSelector:s1]; 与 [p1 sayHi]效果是完全一样的.
3). 调用1个对象的方法有两种.
- [对象名 方法名];
- 手动的为对象发送SEL消息.
-
注意事项:
1). 如果方法有参数 那么方法名是带了冒号的.
2). 如果方法有参数,如何传递参数.
那么就调用另外1个方法.3). 如果有多个参数
- (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
总结:
- 类是以Class对象的形式存储在代码段.
- 如何取到存储类的类对象.
- 如何使用类对象调用类的类方法
- 方法是以SEL数据的形式存储的.
- 调用方法的两种方式.
点语法
-
Java、 C# 对象可以使用点语法来访问对象的成员.
OC中也有点语法. OC中也可以使用点语法来访问对象的属性.
但是OC的点语法和Java C# 是完全不一样的.OC的对象如果要为属性赋值或者取值 就要调用对应的getter或者setter.
-
使用点语法来访问对象的属性.
语法:
对象名.去掉下划线的属性名;
p1.name = @"jack";
这个时候就会将@"jack"
赋值给p1
对象的_name
属性.
NSString *name = p1.name;
把p1
对象的_name
属性的值取出来. -
点语法的原理.
p1.age = 18;
这句话的本质并不是把18直接赋值给p1对象的_age属性.点语法在编译器编译的时候.其实会将点语法转换为调用setter、getter的代码.
(这就是懒加载的本质。self.属性触发getter方法,实现懒加载)
1). 当使用点语法赋值的时候. 这个时候编译器会将点语法转换为调用setter方法的代码.
对象名.去掉下划线的属性名 = 数据;
转换为:[对象名 set去掉下划线的属性名首字母大写:数据];
p1.age = 10;
[p1 setAge:10];
2).当使用点语法取值的时候.这个时候编译器会将点语法转换为调用getter方法的代码.
对象名.去掉下划线的属性名;
转换为:
[对象名 去掉下划线的属性名];int age = p1.age;
int age = [p1 age]; -
注意.
1). 在getter和setter中慎用点语法,因为有可能会造成无限递归 而程序崩溃,
2). 点语法在编译器编译的时候 会转换为调用setter getter方法的代码.
p1.name = @"jack";
[p1 setName:@"jack"]
NSString *name = p1.name;
NSString *name = [p1 name];
如果我们的setter方法和getter方法名不符合规范 那么点语法就会出问题.
3). 如果属性没有封装getter setter 是无法使用点语法的
因为点语法的本质是getter setter方法.
property(重点,阐述了属性写法的发展过程)
-
我们写1个类.
a. 要先为类写属性.(成员变量)
b. 在声明属性的getter setter
c. 再实现getter setter哎! 1点点技术含量都没有.有没有更为简单的方式来实现同样的代码效果呢
-
@property
1). 作用: 自动生成getter、setter方法的声明.
因为是生成方法的声明,所以应该写在@interface类的声明之中.2). 语法:
@property 数据类型 名称;
@property int age;
3). 原理:
编译器在编译的时候.会根据@property生成getter和setter方法的声明!@property 数据类型 名称;
生成为:- (void)set首字母大写的名称:(数据类型)名称; - (数据类型)名称; eg: @property int age; //就会声明下面的句子 - (void)setAge:(int)age; - (int)age;
-
使用@property注意.
1). @property的类型和属性的类型一致.
@property的名称和属性的名称一致(去掉下划线)
不要乱写.2). @property的名称决定了生成的getter和setter方法的名称.
所以,@property的名称要和属性的名称一致 去掉下划线 否则生成的方法名就是不符合规范的
@property的数据类型决定了生成的setter方法的参数类型 和 getter方法的返回值类型.3). @property只是生成getter和setter方法的声明. 实现还要自己来. 属性还要自己定义.
synthesize
-
@property 只能生成getter和setter的声明.
实现还要我们自己来.而实现也是没有什么任何技术含量. 方法实现的代码能不能也可以自动生成呢?
-
@synthesize
1).作用: 自动生成getter、setter方法的实现. 所以,应该写在类的实现之中. 2).语法: @synthesize @property名称; //这就是我之前再看前辈们代码时候他们的写法(我困惑了很久哈哈) @interface Person : NSObject { int _age; } @property int age; @end @implmentation Person @synthesize age; @end 3).@synthesize做的事情. @implmentation Person @synthesize age; @end --- @implementaion Person { int age; } - (void)setAge:(int)age { self->age = age; } - (int)age { return age; } @end a. 生成1个真私有的属性.属性的类型和@synthesize对应的@property类型一致. 属性的名字和@synthesize对应的@property名字一致. b. 自动生成setter方法的实现. 实现的方式: 将参数直接赋值给自动生成的那个私有属性.并且没有做任何的逻辑验证. c. 自动生成getter方法的实现. 实现的方式: 将生成的私有属性的值返回.
-
希望@synthesize不要去自动生成私有属性了.
getter setter的实现中操作我们已经写好的属性就可以了.语法:
@synthesize @property名称 = 已经存在的属性名;
@synthesize age = _age;
1). 不会再去生成私有属性.2). 直接生成setter getter的实现,
setter的实现: 把参数的值直接赋值给指定的属性.
gettter的实现: 直接返回指定的属性的值. -
注意:
1). 如果直接写1个@synthesize
@synthesize name;2). 如果指定操作的属性.
@synthesize name = _name;3). 生成的setter方法实现中 是没有做任何逻辑验证的 是直接赋值.
生成的getter方法的实现中 是直接返回属性的值.
如果setter或者getter有自己的逻辑验证 那么就自己在类的实现中重写就可以了.
-
批量声明(这个我还真没有怎么使用过,以后可以使用一下)
1). 如果多个@property的类型一致. 可以批量声明.
@property float height,weight;
2). @synthesize 也可以批量声明.
@synthesize name = _name,age = _age,weight = _weight,height = _height;
property增强
@property只是生成getter setter 的声明.
@synthesize是生成getter setter 的实现.
这种写法是Xcode4.4之前的写法. 从Xcode4.4以后.Xcode对@property做了1个增强
@property增强
只需要写1个@property 编译器就会自动
- 生成私有属性.
2).生成getter setter的声明.
3).生成getter setter的实现.
@property NSString *name;
做的事情
1). 自动的生成1个私有属性,属性的类型和@property类型一致 属性的名称和@property的名称一致 属性的名称自动的加1个下划线.
2). 自动的生成这个属性的getter setter方法的声明
3). 自动的生成这个属性的getter setter方法的实现.
setter的实现: 直接将参数的值赋值给自动生成的私有属性.
getter的实现: 直接返回生成的私有属性的值.
使用注意
1). 名称要和属性的名称一致 只是去掉下划线.
2). 也可以批量声明相同类型的@property
3). @property生成的方法实现没有做任何逻辑验证.
setter: 直接赋值
getter: 直接返回.
所以,我们可以重写setter来自定义验证逻辑.如果重写了setter 还会自动生成getter
如果重写了getter 还会自动生成setter
这个注意点从来没注意过:
如果同时重写getter setter 那么就不会自动生成私有属性了.
解决方法就是自己在大括号里写一个私有属性嘛
如果你想为类写1个属性 并且为这个属性封装getter setter
1个@property就搞定.
继承.
父类的@property一样可以被子类继承.
@property生成的属性是私有的 在子类的内部无法直接访问生成的私有属性。
但是可以通过setter getter来访问。
动态类型和静态类型
-
OC是1门弱语言.
编译器在编译的时候.语法检查的时候没有那么严格.
不管你怎么写都是可以的.int num = 12.12;
优点: 灵活 咋个行都写.
缺点: 太灵活强类型的语言: 编译器在编译的时候 做语法检查的时候 行就是行 不行就是不行.
静态类型:
指的是1个指针指向的对象是1个本类对象.
动态类型:
指的是1个指针指向的对象不是本类对象.-
编译检查.
编译器在编译的时候,能不能通过1个指针去调用指针指向的对象的方法.
判断原则: 看指针所属的类型之中是有这个方法,如果有就认为可以调用 编译通过.
如果这个类中没有 那么编译报错.这个叫做编译检查. 在编译的时候 能不能调用对象的方法主要是看指针的类型.
我们可以将指针的类型做转换,来达到骗过编译器的目的.
-
运行检查.
编译检查只是骗过了编译器. 但是这个方法究竟能不能执行.
所以在运行的时候.运行时会去检查对象中是否真的有这个方法.如果有就执行 如果没有就报错误.
-
LSP 里氏替换原则
父类指针指向子类对象.
实际上任意的指针可以执行任意的对象.编译器是不会报错的.
当1个子类指针执行1个父类对象的时候,编译器运行通过子类指针去调用子类独有的方法.
但是在运行的时候是会出问题的.因为父类对象中根本没有子类成员.
动态类型检测
LLVM(苹果编译器的名字)
-
编译检查.
编译器在编译的时候. 判断1个指针是否可以调用指向的对象的方法.
判断的准则就是指针的类型.
我们可以很轻松的把编译器给骗过.
-
就算骗过了编译器,程序在运行的时候还会做运行检查.
我们写的程序就算编译通过了.不意味着可以完美的执行
-
我们就希望.我们可以写代码来先判断1下.对象中是否有这个方法.如果有再去执行.
如果没有就别去执行.
1). 判断对象中是否有这个方法可以执行.
-(BOOL)respondsToSelector:(SEL)aSelector;
最常用的是这个方法.
2). 判断类中是否有指定的类方法.
-(BOOL)instancesRespondToSelector:(SEL)aSelector;
3). 判断指定的对象是否为 指定类的对象或者子类对象.
-(BOOL)isKindOfClass:(Class)aClass;
BOOL b1 = [s1 isKindOfClass:[Person class]];
判断s1对象是否为Person对象或者Person的子类对象.
4). 判断对象是否为指定类的对象 不包括子类.
(BOOL)isMemberOfClass:(Class)aClass;
[s1 isMemberOfClass:[Student class]];
判断s1对象是否为1个Student对象. 不包括Student的子类对象.5). 判断类是否为另外1个类的子类.
(BOOL)isSubclassOfClass:(Class)aClass;
id类型
-
NSObject.
是OC中所有类的基类.根据LSP NSObject指针就可以指向任意的OC对象.
所以.NSObject指针是1个万能指针.可以执行任意的OC对象.
缺点: 如果要调用指向的子类对象的独有的方法.就必须要做类型转换.
-
id指针.
是1个万能指针,可以指向任意的OC对象.
1). id是1个typedef自定义类型 在定义的时候已经加了*
所以,声明id指针的时候不需要再加*了.- id指针是1个万能指针,任意的OC对象都可以指.
-
NSObject和id的异同.
相同点: 万能指针 都可以执行任意的OC对象.
不同点: 通过NSObject指针去调用对象的方法的时候.编译器会做编译检查.
通过id类型的指针去调用对象的方法的时候,编译器直接通过.无论你调用什么方法.
注意: id指针只能调用对象的方法 不能使用点语法.如果使用点语法就会直接报编译错误 。
如果我们要声明1个万能指针 千万不要使用NSObject 而是使用id
-
父类中的类方法创建1个父类对象返回.
1). 如果返回值写为父类类型的.那么子类来调用这个方法得到的就是父类指针.
解决的方式: 把返回值改为id类型的.
2). 方法的内部创建的对象的是 不要写死. 因为写死创建的对象就固定了.
我们希望那1个类来调用这个方法就创建那1个类的对象.
把类名写为self 哪1个类来调用这个方法 self就指的是那1个类.创建的就是那1个类的对象.
3). 方法的返回值是id类型的.问题就是任意指针都可以接收这个方法的返回值.
编译器连个警告都没有.如果方法的返回值是
instancetype
代表方法的返回值是当前这个类的对象.5). 使用建议
-
如果方法内部是在创建当前类的对象,不要写死成类名
[类名 new]
;而是用self代替类名.
如果方法的返回值是当前类的对象,也不要写死了. 而是写instancetype
6). id和instancetype的区别.
-
instancetype只能作为方法的返回值.不能在别的地方使用.
id既可以声明指针变量 也可以作为参数 也可以作为返回值.
-
instancetype 是1个有类型的 代表当前类的对象.
id是1个无类型的指针 仅仅是1个地址.没有类型的指针.
-
构造方法
-
创建对象,我们之前说:
类名 *指针名 = [类名 new];
new实际上是1个类方法.
new方法的作用:
-> 创建对象。->初始化对象
-> 把对象的地址返回.
new方法的内部,其实是先调用的
alloc
方法. 再调用的init
方法.alloc
方法是1个类方法,作用: 哪1个类调用这个方法 就创建哪个类的对象,并把对象返回.
init
方法 是1个对象方法,作用: 初始化对象.创建对象的完整步骤:
应该是先使用alloc创建1个对象,然后再使用init初始化这个对象 才可以使用这个对象.
虽然没有初始化的对象 有的时候 也可以使用. 但是千万不要这么做.
使用1个未经初始化的对象是极其危险的.
Person *p1 = [Person new];
完全等价于
Person *p1 = [[Person alloc] init];
-
init方法.
作用: 初始化对象,为对象的属性赋初始值 这个init方法我们叫做构造方法.
init方法做的事情:初始化对象.
为对象的属性赋默认值.
如果属性的类型是基本数据类型就赋值为0
C指针 NULL
OC指针 nil所以.我们创建1个对象如果没有为这个对象的属性赋值 这个对象的属性是有默认值的.
所以,我们每次新创建1个对象,这个对象的属性都被初始化了. 我们想要让创建的对象的属性的默认值不是 nil NULL 0
而是我们自定义的.
那么这个时候,我们就可以重写init方法. 在这个方法中按照我们自己的想法为对象的属性赋值.
重写init方法的规范:
1). 必须要先调用父类的init方法.然后将方法的返回值赋值给self
2). 调用init方法初始化对象有可能会失败,如果初始化失败.返回的就是nil
3). 判断父类是否初始化成功. 判断self的值是否为nil 如果不为nil说明初始化成功.
4). 如果初始化成功 就初始化当前对象的属性.
5). 最后 返回self的值.
解惑
1). 为什么要调用父类的init方法.
因为父类的init方法 会初始化父类的属性(比如父类的真私有属性). 所以必须要保证当前对象中的父类属性也同时被初始化.
2). 为什么要赋值给self?
因为.调用父类的init方法 会返回初始化成功的对象
实际上返回的就是当前对象。但是我们要判断是否初始化成功.
无论如何,记住重写init方法的规范.
-(instancetype)init
{
if(self = [super init])
{
//初始化当前类的属性的代码;
}
retrun self;
}
什么时候需要重写init方法:
如果你希望创建出来的对象的属性的默认值不是 nil NULL 0 而是我们指定的值.
那么这个时候我们就可以重写init方法.
重写init方法以后.
稍稍不爽的: 这样每次创建出来的对象的属性的值都是一样的.
创建对象的时候,对象的属性的值由创建对象的人来指定.而不是写死在init方法中
自定义构造方法.
规范:
1). 自定义构造方法的返回值必须是instancetype
2). 自定义构造方法的名称必须以initWith开头.
3). 方法的实现和init的要求一样.