self关键字:
在对象方法中,父类的成员对于子类来说,也是属于子类的。所以父类的成员在子类的方法中也可以使用self关键字来访问。
多态的前提:有方法的重写。
89. 子类在内存中的存储和方法调用过程。
一、创建一个对象,对象在内存中是如何分配的。
1). 子类对象中有自己的属性,同时拥有所有父类的属性。
2). 代码段中的每1个类,都有1个叫isa的指针,这个指针指向它的父类。
一直指到NSObject。
[p1 sayHi];
在调用方法的时候,先根据p1指针找到p1指向的对象,然后根据对象的isa指针找到Person类。
搜索Person类中是否有这个sayHiffa ,如果有就执行;
如果没有,就根据类的isa指针找父类.....
NSObject 类都没有,就报错。
90. 结构体与类的区别
结构体与对象的相同点
都可以将多个数据封装为1个整体。
结构体与对象的不同点
a. 结构体只能封装数据,类不仅可以封装数据 还可以封装行为。
b. 结构体变量分配在栈空间(局部变量的情况下);
而对象分配在堆空间。
----栈的特点:空间相对较小,但是存储在栈中的数据,访问的效率更高一些。
----堆的特点:空间相对较大,但是存储在堆中的数据,访问的效率相对低一些。
----栈中存储的数据访问效率高;堆中存储的数据访问效率低。
c. 赋值:
结构体:Student
类:Person
Student s1 = {"jack", 19, GenderMale};
Student s2 = s1; // 值赋值。copy赋值
Person *p1 = [Person new];
Person *p2 = p1; // 对象的地址赋值。
应用场景。
a. 如果表示的这个实体,不仅由多个数据组成,还有行为,只能使用类。
b. 如果表示的实体没有行为,光有属性。
-- 如果属性较少,只有结果。那么这个时候就定义为结构体,分配在栈,提高效率。
-- 如果属性较多,不要定义为结构体。因为这样结构体变量会在栈中占据很大的1块空间,反而会影响效率。
typedef struct {
int year;
int month;
int day;
} Date; // 日期结构体
@interface Book: NSObject {
NSString *_name;
Auther *_auther; // 作者类
Date _publishDate; // 结构体:不需要加星号 *
}
Book *b1 = [Book new];
[b1 setName:@"钢铁侠"];
[b1 setPublishDate:{2020, 11, 22}];
Author *a1 = [Author new];
[a1 setName:@"奥斯特洛夫斯基"];
[b1 setAuthor: a1];
91. 类的本质:类是以class对象,存储在代码段
类加载:当类第1次被访问的时候,这个类就会被夹在到代码段存储起来。
- 类什么时候,加载到代码段?
类第1次被访问的时候,这个类就会被夹在到代码段存储起来。类加载。
类以什么样的形式,存储在代码段?
类一旦被加载到代码段之后,什么时候回收?
是不会被回收的,除非程序结束。
类以什么样的形式存储在代码段的?
- 任何存储在内存中的数据,都有1个数据类型。
- 在代码段存储类的那一块空间,是什么类型的?
在代码段中,存储类的步骤。
---a. 先在代码段中,创建一个Class对象,Class是Foundation框架中的1个类。
这个Class对象,就是用来存储类信息的。
---b. 将类的信息,存储在这个Class对象之中。
这个Class对象,至少有三个属性:
------类名:存储的这个类的名称;
------属性们:存储的这个类,具有那些属性。
------方法们:存储的这个类,具有那些方法。
所以。类是以Class对象的形式,存储在代码段的。
存储类的这个Class对象,也叫做类对象,用来存储类的1个对象。
所以,存储类的类对象,也有一个叫isa指针的属性,这个指针指向存储其父类的类对象。
92. 类对象Class的使用
- 如何拿到存储在代码段中的类对象。
a. 调用类的类方法class
,就可以得到存储类的类对象的地址。
Class c1 = [Person class]; // Class类型本身带*
NSLog(@"c1 = %p", c1);
b. 调用对象的对象方法class
,就可以得到存储这个对象所属的类的Class对象的地址。
Person *p1 = [Person new];
Class c2 = [p1 class];
NSLog(@"c2 = %p", c2);
c. 对象中的isa指针的值,其实就是代码段中存储类的类对象的地址。
注意:
声明Class指针的时候,不需要加号,因为在typedef
的时候已经加了号了。
- 如何使用类对象----Class
a. 拿到存储类的类对象后。
Class c1 = [Person class];
c1对象就是Person类。
c1 完全等价于 Person
b。使用类对象来调用类的类方法。
因为类对象,就代表存储在这个类对象中的类。c1就是Person类。所以在使用Person的地方,完全可以使用c1代替。
[Person sayHi];
等价与 [c1 sayHi];
c. 可以使用类对象,来调用new方法,来创建存储在类对象中的 类的对象。
Person *p1 = [Person new];
Class c1 = [Person class];
Person *p2 = [c1 new];
d. 注意:
使用类对象,只能调用类的方法,因为类对象就等价于存在其中的类。
Class c1 = [Person class];
c1就是Person。
e:总结:
------ 类是以Class对象的形式,存储在代码段之中的。
------ 如何拿到存储类的类对象。
------ 作用:可以使用类的对象,调用类的类方法。
Class c1 = [Person class]; 要调用Person的类方法,可以使用Person去调用,也可以使用c1去调用。
93. 方法的本质:SEL对象。
- SEL:全称是selector选择器。
SEL是1个数据类型,所以需要在内存中申请 空间存储数据。
SEL其实是1个类, SEL对象是用来存储1个方法的。
- 类是以Class对象的形式,存储在代码段之中的。
类名:存储这个类的类名。NSString.
还要将方法存储在类对象之中,如何将方法存储在类对象之中。
- 先创建1个SEL对象。
- 将方法的信息,存储在这个SEL对象之中。
- 再将这个SEL对象,作为类对象的属性。
- 拿到存储方法的SEL对象。
- 因为SEL是1个typedef类型的,在自定义的时候已经加了。
所以在声明SEL的时候,不需要加。 - 取到存储方法的SEL对象。
SEL s1 = @selector(sayHi);
NSLog(@"s1 = %p", s1);
- 调用方法的本质:
[p1 sayHi];
先对象发送SEL消息。
a. 先拿到存储sayHi方法的SEL对象,也就是拿到存储sayHi方法的SEL数据。SEL消息。
b. 将这个SEL消息,发送给p1对象。
c. 这个时候,p1对象接收到SEL消息后,就知道要调用方法。
d. 根据对象的isa指针,找到存储类的类对象。
e. 找到这个类对象之后,在这个类对象中去搜索是否有和传入的SEL数据相匹配的。
如果有,就执行。
如果没有,再找父类,直到找到NSObject.
OC最重要的1个机制:消息机制。
调用方法的本质,其实就是为对象发送SEL消息。
[p1 sayHi];
为p1对象,发送1条sayHi消息。
94. 手动向对象发送SEL消息。
5. 重点概念:
- 方法是以SEL对象的形式存储起来的。
-
如何拿到存储方法的SEL对象。
使用1个SEL对象存储1个方法方法。
6. 手动的为对象发送SEL消息。
- 先得到方法的SEL数据.
- 将这个SEL消息发送给p1对象。
- 调用1个对象的方法有两种形式:
----- [对象名 方法名];
----- 手动的为对象发送SEL消息。
Person *p1 = [Person new];
SEL s1 = @selector(sayHi); // 拿到sayHi方法的SEL对象。
[p1 performSelector:s1]; // 为p1发送一条s1消息(调用sayHi方法)。等价于`[p1 sayHi];`
7. 注意事项:
- 如果方法有参数,那么方法名是带了冒号的。
- 如果方法有参数,如何传递参数?(调用带withObject的方法)
Person *p1 = [Person new];
SEL s1 = @selector(eatWithFood:); // 拿到sayHi方法的SEL对象。
[p1 performSelector:s1 withObject:@"红烧肉"]; // 传递参数。
- 如果有多个参数,把参数封装在1个对象里面,
8.总结:
- 类是以Class对象的形式存储在代码段。
- 如何取到存储类的类对象。
- 如何使用类对象,调用类的类方法。
- 方法是以SEL数据的形式存储的。
- 调用方法的两种形式。
95. 点语法
Java、C# 对象可以使用点语法,来访问对象的成员。
OC中也有点语法。OC中也可以使用点语法来访问独享的睡醒。
使用OC的点语法和Java、C# 是完全不同的。
OC对象想要为属性赋值或取值,就要调用对应的getter或setter方法。
1. 使用点语法来访问对象的属性。
语法:对象名.去掉下划线的属性名
Person *p1 = [Person new];
p1.name = @"Jack"; // 给属性赋值
NSString *name = p1.name; // 取p1对象name属性中的值
2. 点语法的原理
p1.age = 18;
这句话的本质,并不是把18直接赋值给p1对象的_age属性。
点语法在编译器编译的时候。其实会将点语法转换成调用setter,getter的代码。
当使用点语法赋值的时候,这个时候编译器会将点语法,转换为调用setter方法的代码。
对象名.去掉下划线的属性名 = 数据;
转换为: [对象名 set去掉下划线的属性名,首字母大写:数据];
p1.age =10;
<====== > [p1 setAge:10];
当使用点语法取值的时候,这个时候编译器会将点语法,转换为调用getter方法的代码。
对象名.去掉下划线的属性名;
转换为: [对象名 去掉下划线的属性名];
int age = p1.age
<====== > int age = [p1 age];
3. 注意:
在setter和getter里面,慎用点语法,容易造成死循环(无限递归调用)。
点语法。最终在编译期编译的时候,会转换为setter,getter方法的代码。
如果setter方法,getter方法名不符合规范,那么点语法就会出问题。
p1.name = @"jack";
[p1 setName:@"jack"];
NSString *name = p1.name;
NSString *name = [p1 name];
想要使用点语法,必须为属性创建getter,setter方法。
如果属性没有封装getter,setter方法,是无法使用点语法的。
因为点语法的本质,是调用getter,setter方法。
96. @property关键字:【编译器自动的,生成getter,setter方法】
写一个类:
- 先写一个类,声明属性。
- 再声明属性的getter,setter。
- 再实现getter,setter方法。
编译器自动的,生成getter,setter方法。-------@property
- 作用:自动生成getter、setter方法的声明。
因为是生成方法的声明,所以应该写在@interface类的声明当中。 - 语法:
@property 数据类型 名称;
@property int age; - 原理:自动生成getter,getter方法的声明。
编译器在编译的时候,会根据@property 生成getter和getter方法的实现。
@property 数据类型 名称;
生成:
- (void)set首字母大写的名称:(数据类型)名称;
- (数据类型)名称
@interface Person: NSObject {
NSString *_name;
int _age;
}
@property int name;
/*
-(void)setName:(NSString *)name;
-(NSString *)name;
*/
@property int age;
/*
-(void)setAge:(int)age;
-(int)age;
*/
@end
使用@property的注意:
- @property的类型要和属性的类型,要一致。
@property的名称与属性名称要一致(去掉下划线),不能乱写。
- @property的类型要和属性的类型,要一致。
- @property的名称决定了生成getter和setter方法的名称。
所以,@property的名称,要和属性的名称一致(去掉下划线),否则生成的方法名,就是不符合规范的。
@property的数据类型,决定了生成setter方法的参数类型,getter方法的返回值类型。
- @property的名称决定了生成getter和setter方法的名称。
- @property只是生成getter和setter方法的声明,实现还要自己来,属性还要自己定义。
97. @synthesize关键字。【自动生成getter,setter方法的实现】
@property只能生成getter和setter的声明,方法的实现还是急需要自己来。
- 作用:自动生成getter,setter方法的实现。所以,应该写在类的实现当中。
- 语法:@synthesize @property 名称;
@interface Person: NSObject{
int _age;
}
@property int age;
@end
-----------------------
@implementation Person
@synthesize age; // 自动生成age setter,getter的实现。
@end
- @synthesize做的事情:
a. 生成1个真私有的属性。属性的类型和@synthesize对应的@property类型一致。
属性的名字和@synthesize对应的@property名字一致。
b. 自动生成setter方法的实现,
实现的方式:将参数直接赋值给自动生成的那个私有属性。
并且没有做任何的逻辑验证。
// @synthesize age ====> 自动生成的代码。
@implementation Person {
int age
}
- (void) setAge:(int)age {
self->age = age;
}
- (int) age {
return age;
}
c. 自动生成getter方法的实现。
实现的方式:将生成的私有属性的值返回。
3.自动生成的私有属性,有点多此一举。期望@synthesize不要自动生成私有属性。
getter,setter的实现,操作我们自己写好的属性即可。方法的实现,都操作自己写好的属性。
语法:@synthesize @property 名称= 已经存在的属性名;
@synthesize @property age = _age;
- 不会再去生成私有属性,直接生成getter,setter的实现。
setter的实现:把参数的值,直接赋值给指定的属性。
getter的实现:直接返回指定属性的值。
2。方法的实现:
/*
- (void)setAge:(int)age {
_age = age;
}
- (int)age {
return _age;
}
*/
4. 注意
- 如果直接写一个@synthesize,
@synthesize name;
自动生成私有属性,方法的实现操作的是自动生成的私有属性。 - 如果指定操作的属性:
@synthesize name = _name;
不会生成私有属性,getter,setter里面操作的是指定的属性。 - 生成的 setter方法实现中,是没有任何逻辑判断的,直接赋值。
生成的 getter方法的实现中,是直接返回属性的值。 - 如果setter,getter有自己的逻辑验证,自己在类的实现当中重写就可以了。
- (void)setAge:(int)age{
if(age>=0 && age<=20) {
_age = age;
} else {
_age = 18;
}
}
5. 批量声明
- 如果多个@property的 类型一致, 可以批量声明。
@property float height, weight;
- @synthesize 也可以批量写。类型不一致,也可以写。
@synthesize name=_name, age=_age, weight=_weight, height=_height;
// 一行抵好几行。
98. @property 增强 *****
@property 只是自动生成getter,setter的声明;
@synthesize 是生成getter,setter的实现。
从Xcode4.4以后, Xcode编译器对@property做了增强。
只需要写1个@property ,编译器就会自动:
a. 生成私有属性;
b. 生成getter,setter的声明;
a. 生成getter,setter的实现。
@interface Student : NSObject
@property NSString *name;
@end
做的事情:
- 自动的生成1个私有属性,属性的类型与@property类型一致,属性的名称和@property的名称一致,
属性的名称自动的加1个下划线。_name - 自动的生成这个属性的getter,setter方法的声明。
- 自动的生成这个属性的getter,setter方法的实现。
setter的实现:直接将参数的值,赋值给自动生成的私有属性。
getter的实现:直接返回生成的私有属性的值。
3. 使用注意:
@property的类型和名字,决定了生成的属性的类型和名字。
名称要和属性的名称一致,只是去掉下划线。@property 也可以批量声明系统类型的属性,批量声明的时候,会自动生成n个属性。
@property 生成的方法实现,没有做任何逻辑验证。
setter,直接赋值; getter,直接返回。可以重写属性对应的setter或getter方法,来实现自定义的功能。
可以重写setter来自定义验证逻辑。如果同时重写getter,setter那么就不会生成私有属性,需要自己在{} 中声明。
父类的@property一样可以被子类继承, @property 生成的属性是私有的,在子类中无法直接访问生成的私有属性。
但是可以通过setter,getter来访问。
99. 静态类型和动态类型
OC是一门弱语言,编译器在编译的时候,检查的时候没有那么严格。不管怎么写都可以。
int num = 12.12;
强类型的语言:java, C#
2. 静态类型
指的是1个指针指向的对象是1个本类对象
Pig *p1 = [Pig new];
动态类型:
指的是1个指针指向的对象不是1个本类对象
Animal *a1 = [Pig new];
3. 编译检查(检查指针的类型)
编译器在编译的时候,能不能通过1个指针去调用 指针指向的对象的方法。
判断原则:看指针所属的类型之中是否这个方法。
---有,就可以调用,编译通过;
---没有,那么编译报错。
在编译的时候, 能不能调用对象的方法,主要看指针的类型。跟对象是什么对象,没关系。
可以将指针的类型做转换,来达到骗过编译器的效果。
Animal *a1 = [Pig new];
[(Pig *)a1 eat];
4. 运行检查(检查对象的类型)
编译检查,只是骗过了编译器。但是方法究竟能不能执行?
所以,在运行的时候,还是会再次进行检查对象中是否有这个方法。没对应的方法,会报错。
Pig *p1 = [Animal new];
[p1 run];
// 正常执行
[p1 eat];
// 报错。
5. LSP 父类指针指向子类对象。
实际上,任意的指针可以指向任意的对象。编译器是不会报错的。
当1个子类指针,指向父类对象,编译器运行通过子类指针,去调用子类独有的方法。
但是运行的时候,会出问题。因为父类对象中,根本没有子类成员。
100. NSObject指针 和 id指针。
1. NSObject 指针。
是OC中所有类的基类。根据LSP,NSObject指针,可以指向任意的OC对象。
所以:NSObject指针,是1个万能指针。可以指向任意的OC对象。
NSObject *obj1 = [Person new];
缺点:如果要调用指向的子类对象的独有的方法,就必须要做类型转换。
NSObject *obj3 = @"Jack";
[(NSString *) obj3 length];
2. id指针(万能指针,可以指向任意的OC对象)
id是一个typedef类型,在定义的时候已经带了。所以,声明id指针不要再带。
id指针是1个万能指针,任意的OC对象都可以指。
id id1 = [Person new];
3. NSObject, id的异同:
相同点:万能的指针,都可以指向任意的OC对象。
不同点:
通过NSObject指针,去调用对象的方法的时候。编译器会做编译检查。
通过id类型的指针,去调用对象的方法的时候,编译器直接通过,不做编译检查。
id不要做类型转换。声明万能指针,建议用id。
NSObject *obj = [Person new];
// [obj sayHi]; // 编译报错
id id1 = [Person new];
[id1 length]; // 正常编译通过。执行的时候,会报错。因为对应的对象里面,没有length方法。
[id1 sayHi]; // 编译正常,执行也正常。
4. 注意点:
id指针只能调用方法,不能使用点语法;如果使用点语法,直接报编译错误。
如果要使用setter方法,调用对应的setter方法实现。
如果要声明1个万能指针,不要使用NSObject,而要用id,因为id方便。