------ Java培训、Android培训、iOS培训、.Net培训 、期待与您交流! -------
1.点语法
本质:是方法调用,并不是访问成员变量,所以前边的声明是不能少的
p.age = 10; 等价 [p setAge:10];
int a = p.age; 等价 [p age];
编译器特性:当使用点语法时,编译器会自动展开称相应的方法。
2.成员变量作用域
基本概念 :局部变量,全局变量,都有自己的作用域,成员变量也不例外
四大类型的成员变量的访问方式:
a.@private : 只能在当前类的实现@implementation中直接访问,也就是对象中,但是可以用set方法和get方法来访问
b.@protect : 可以在当前类及子类的的实现@implementation中直接访问,也就是对象中
c.@public : 任何地方都能通过成员变量名直接访问成员变量,也就是对象中
d.@package:只要处在同一个框架中,就能直接访问(介于@private,@protect之间)
在声明中什么都不写就代表了是protected
在实现中什么都不写代表私有private,但是m文件不被main函数包含,所以是不认的
注意: a.@implementation中也可以定义成员变量但不能与@interface中的变量名相同,所以没有@interface,只有@implementation也可以开发一个类。
b.在@interface中在成员变量默认为@protect,在@implementation中成员变量默认为@private(因为.m文件不会被包含即使在@implementation中使用@public来说明成员变量的类型也没用,还是@private类型;只有.h文件才可以被包含,所以在@interface中使用@public是有作用的)。
3.@property和 @synthesize
@property:可以自动生成,某个成员变量的setter和getter方法声明,如果自己同时写便不会生成。
@property int age;
@synthesize :可以自动生成成员变量的setter和getter方法实现,并且会访问这个成员变量。
@synthesize age = _age;
@synthesize细节:
@synthesize age = _age ;
setter和getter方法实现中默认会访问成员变量 _age。
如果成员变量_age不存在,就会自动生成一个@private(因为@synthesize写在@implementation中)的成员变量_age。
@synthesize age ;
setter和getter方法实现中默认会访问成员变量 age。
如果成员变量age不存在,就会自动生成一个@private。
Xcode4.4之后的特性
@property独揽@synthesize功能,即@property可以同时生成setter和getter的声明和实现,默认情况下,setter和getter方法中的实现会去访问下划线_开头的成员变量。
手动实现
若手动实现setter方法编译器就会生成getter和成员变量(成员变量已存在便不会生成)
若手动实现getter方法编译器就会生成setter和成员变量(成员变量已存在便不会生成)
若同时手动实现setter和getter方法,编译器就不会生成不存在的成员变量(即没有成员变量)也就是没有_age这个变量了,去掉其中一个实现,那么还是有_age的
@interface Car : NSObject
@property int age,height; // 这句话能够做三件事,1:声明private的成员变量_开头的,2:声明set和get方法,3:实现set和get方法
@end
@implementation Car
@endint main()
{
Car *p = [Car new];
// [p setAge:10];
p.age = 10; // 用点语法来代替set方法
//[p age];
int s = p.age; // 用点语法来代替get方法
NSLog(@"%d",s);
return 0;
}
4. 数据类型 id
万能指针,能指向任何OC对象,相当于NSObject *
typedef struct objc object{
Class isa;
}* id; //注意: id后面不要加*
int main()
{
id p = [Car new]; // id相当于NSObject *
[p setAge:10];
//p.age = 10; // 用点语法来代替set方法
//[p age];
int s = [p age]; // 用点语法来代替get方法
NSLog(@"%d",s);
return 0;
}
5.构造方法
new方法和默认的init方法
+new是一个类方法,用来创建一个对象, 但是new方法把所有属性的值初始化为0, 在开发中不常用.
完整的创建出一个可用的对象,需要两步:
1分配存储空间给对象, 返回一个有了存储空间的对象(对象地址) (返回类型是id).
2给对象初始化, 返回一个可用的对象
这两步分别对应一个方法:
1 类方法 +alloc分配空间
2 对象方法 -init 初始化对象 (默认的-init方法初始化所有参数为0)
new方法里面只做这两件事:
1 调用+alloc分类存储空间
Person *p1 = [Person alloc];
2 调用-init进行初始化
Person *p = [p1 init];
#重写init方法#
init就是构造方法,用来初始化对象. 构造方法是对象方法,因为它需要设置成员变量的值.
默认构造方法中所有的成员变量的值都设为0.我们想让新建对象时成员变量的默认值不是0, 可以自己重写构造方法.
-init的方法是父类NSObject的方法.可以在子类中重写.
在类的实现中重写 (重写方法可以不用声明,直接实现就可以)
@implementation Person
- (id) init {
// 必须要先调用父类的init方法进行初始化,因为父类中声明的成员变量子类也有, 需要在init中初始化(比如把isa指针指向当前的类).
self = [super init]; // 初始化父类的成员变量,返回初始化好的对象
// 判断是否初始化成功
if (self != nil) {
// 给当前类的成员变量赋值
_age = 20;
}
// 返回已经初始化完毕的对象
return self;
}
@end
/* 简写方式
- (id) init {
if (self = [super init]) { // 调用父类方法初始化, 并判断self是否有值
_age = 20; // 给成员变量赋值
}
return self; //返回初始化完毕的对象
}*/
# 小结 #
构造方法 -初始化对象的方法.
重写构造方法,可以让对象创建时, 成员变量默认为非0的值.
重写构造方法的步骤:
1 调用父类的init方法,并判断是否为空值
2 初始化子类的成员变量.
3 返回初始化完毕的对象
6.自定义构造方法
重写init方法时,对象的成员变量初始值只能是固定的值. 自定义构造方法可以在初始化时给成员变量赋不同的初值.
自定义的构造方法需要声明和实现 (重写父类的方法不需要声明)
自定义构造方法的规范 :
a.一定是对象方法,一定以-开头
b. 返回值是id类型
c. 方法名以一般init开头 (方便程序员之间的交流,一看就知道是构造方法)
d. 方法内部的格式和重写init的格式相同 :调用super的方法并判断空值,给自己的成员变量赋值, 返回对象
@interface Person : NSObject
@property NSString *name;
@property int age;
// 添加自定义构造方法
- (id) initWithName:(NSString *)name;
- (id) initWithName:(NSString *)name andAge:(int)age;
@end
@implementation Person
// 重写init方法,默认名字SCL
- (id) init {
if (self = [super init]) {
_name = @"SCL";
}
return self;
}
// 实现自定义的构造方法
- (id) initWithName:(NSString *)name {
if (self = [super init]) { // 和重写init方法是同样的步骤
_name = name;
}
return self;
}
- (id) initWithName:(NSString *)name andAge:(int)age {
if (self = [super init]) {
_name = name;
_age = age;
}
return self;
}
@end
int main() {
Person *p = [[Person alloc] initWithName:@"Rose" andAge:18]; // 调用自己写的初始化方法新建对象
return 0;
}
注意,
如果有继承,通常只给自己的成员变量直接赋值, 父类的成员变量通过调用父类的初始化方法对其赋值
假如Student类是Person的子类,比Person类多了一个属性_no,我们想给Student添加初始化方法
@interface Student : Person
@property int no;
- (id) initWithNo:(int)no;
- (id) initWithName:(NSString *)name andAge:(int)age andNo:(int)no;
@end
@implementation Student
- (id) initWithNo:(int)no {
if (self = [super init]) {
_no = no;
}
return self;
}
- (id) initWithName:(NSString *)name andAge:(int)age andNo:(int)no {
if (self = [super init]) {
_no = no;
// _name = name; 错误.因为_name在父类中是@private,不可以通过_name访问
// [self setName:name]; 可以使用set方法设置, 但是不建议
// self.age = age; 也可以使用点语法, 但是也不建议这样做
}
return self;
}
@end
int main() {
Student *s = [[Student alloc] initWithNo:2];
return 0;
}
// 合理的初始化父类提供的属性,最好使用父类的初始化方法
void test()
{
- (id) initWithName:(NSString *)name andAge:(int)age andNo:(int)no {
if (self = [super initWithName:name andAge:age]) { //调用父类的初始化方法
_no = no;
}
return self;
}
结论:父类属性交给父类处理子类的属性子类处理,即这个成员变量属于谁就让谁来初始化
# 分类 Category #
使用场合 :
比如我们已经有一个Person类,有它定义的方法. 我们想给Person类新加一些方法,但是要求不能修改Person类 (Person.h和Person.m文件).
一种方法是使用继承新建一个子类, 就可以包含Person类的所有方法并添加新的方法.
另一种方法是使用分类, 不需要新建类,也可以扩充类的方法.
分类:
扩充类方法,不修改类文件. 可以添加对象方法和类方法.利于团队开发.
分类的使用
一个类可以有很多个分类.每个分类都可以给它添加不同的方法.
/分类的声明
#import "Person.h" // 需要引用主类, 可以调用主类的方法 (比如属性的set/get方法)
@interface Person(MJ) // 分类用 (小括号) 表示
- (void) study;
@end
//分类的实现
#import "Person+MJ.h" // 分类文件的文件名默认为 主类名+分类名
@implementation Person(MJ)
- (void) study {
NSLog(@"Person is studying. ");
}
@end
//分类的使用
#import <Foundation/Foundation.h>
#import "Person.h" // 包含主类文件
#import "Person+MJ.h" // 包含分类文件
int main() {
Person *p = [[Person alloc] init];
[p study]; // 可以对这个类的对象调用分类中的方法
return 0;
}
注意:
a. 只能增加方法,不能增加成员变量. 如果需要增加成员变量,可以考虑创建子类继承.
b. 分类的实现中可以访问主类的成员变量.
c.分类可以重写主类的方法,但是不建议使用. 如果分类中有和原类中同名的方法,会覆盖原类的方法, 这样会导致主类的方法无法再被调用,不建议这么做.
d. 如果不同的分类中有同名的方法,和编译的顺序有关, 会调用最后编译的分类中的这个方法.在 Build Phases - Compile Sources中可以查看.可以看到只编译.m源文件 (.c .cpp), .h文件不会被编译,安装从上到下的顺序编译. 可以调整文件编译的顺序.注意就算把主类放到分类的后面编译, 依然会调用最后编译的分类中的方法. (Link Binary with Libraries是链接的框架, 就是命令行的 -framework XXX,可以添加其它框架,告诉Xcode哪些框架需要链接)
也就是说, 如果分类中有重名的方法,会相互覆盖, 也会覆盖掉主类中同名的方法.分类中不建议写同名的方法
方法调用的优先级:
调用方法时,会先在分类中寻找,如果没有,再到原类中寻找,如果没有,再到父类中寻找,如果没有,再到头文件中寻找.
7.类对象
什么是类对象
在OC中, 类其实也是一个对象, 是Class类型的对象, 也叫类对象.
Person *p = [[Person alloc] init];
Person类其实也是个对象. Person对象是Person类型的对象. Person类是Class类型的对象.
Person对象叫做实例对象, Person类这个对象叫做类对象.
类名就代表一个类对象. (也就是说, 每个类都只有一个类对象)
创建Person类其实就是使用Class类创建Person类这个对象. (创建Person对象就是使用Person类创建对象)
Class类型的定义
typedef def struct objc_class *Class;
可以看到Class类型的定义中包含了星号.
#类对象的获取#
可以通过对象或者类的class方法返回类对象. class方法返回Class类型的对象(星号被省略), 即获取内存中的类对象.
Class c = [p class]; // c就是Person类对象.
Class c2 = [p2 class]; // c2和c是同一个对象, 可以打印它们的地址, 是一样的.
Class c3 = [Person class]; // c3和c和c2都是同一个类对象
注意:
Class这个名字里已经包含星号了, 所以使用时不用再写星号.
#类对象的使用#
前面讲到, 先加载类对象, 再通过类对象来创建实例对象. 可以通过class方法来获取内存中的类对象(指针).
类对象c和Person等价. Person类名其实就是这个类对象.
可以通过类对象来调取类方法.
比如Person类中有一个 +(void)test 方法
[Person test];等价于[c test];
也就是说 [[p class] test]; 和 [Person test]; 是一样的. [p class] 和 Person 是同一个类对象.
通过类对象指针来创建实例对象 (new是类方法)
Person *p = [c new]; Person *p2 = [Person new];作用是一样的.
#类的加载和初始化#
类的 +load方法 和 +initialize 方法
a.在程序启动时, 系统会先把程序中所有的类和分类都加载进内存(即使没使用这个类也会加载). 加载时会调用类的 +load方法, 只调用一次。且先加载原始类在加载分类。
b.第一次使用某个类的时候会调用+initialize方法对类进行初始化, 只调用一次。如果有分类, 也会被加载和初始化.
注意:
a.当需要调用类的初始化方法时 ,如果分类中重写了initialize方法, 不会调用主类的+initialize方法.
b.+load方法是在加载时调用的, 所以主类和分类的+load方法都会被调用.
初始化方法的应用
重写+initialize方法来监听一个类在什么时候被第一次使用. 或者在类第一次被使用时需要做一些操作就要写在+initialize里.
# 小结 #
加载
a.当程序启动时, 会加载程序中的所有类(父类, 子类, 和分类). 并在加载后调用所有类(父类, 子类, 分类)的+load方法. 只调用一次.
b.先加载父类, 再加载子类, 再加载分类.(即先加载原始类在加载分类)
c.每个类的+load方法只调用一次
初始化
a.当第一次使用某个类时, 会调用当前类的+initialize方法.
b.如果这个类有父类, 父类也要初始化. 即先初始化父类, 再初始化当前类.
c.如果这个类有分类, 分类的的初始化方法会覆盖主类的初始化方法, 所以需要调用主类的初始化方法时, 其实调用的是分类的初始化方法.
d.每个类的+initialize方法只调用一次
8.description方法
调用:
description方法是NSObject自带的方法. 有类方法和对象方法。
作用:
输出类或对象的属性。
当使用NSLog(@"%@")打印对象时, 就调用了对象的 -description 方法, 把-description方法返回的字符串输出到屏幕上. 默认输出的是对象的类名和地址,<类名:内存地址> 可以通过重写description方法打印出对象的内容.
#重写-description方法#
@implementation Person
- (NSString *)description {
// 需要返回一个字符串
return [NSString stringWithFormat:@"Person: age=%d, name=%@", _age, _name];
}
@end
#关于+description方法#
调用:
Class c = [Person class]; // 获得类对象
NSLog(@"%@", c); // 即 NSLog(@"%@", [Person class]); 不可以直接使用类名Person, 必须使用一个Class对象
作用:
输出类或对象的属性。
当使用类对象来调用NSLog(@"%@"), 会调用类的+description方法, 返回NSString*并打印. 默认输出的是类名. 可以重写+description来输出其它内容.
#小结 #
使用NSLog和%@打印时:
如果打印的是实例对象, 调用-description方法, 默认打印类名和对象地址.
如果打印的是类对象, 调用+description方法, 默认打印的是类名.
9.SEL数据类型
含义:
SEL类型的数据代表方法. 对象在调用方法时, 会把方法转换成SEL类型的数据.或者说, 发消息其实发送的就是SEL数据. (消息机制说的就是SEL的使用)SEL的全称是selector.
方法在内存中的存储
假设Person类含有两个方法, 一个类方法+test 和一个对象方法 -test2(它们可以同名).程序运行时, 首先给Person类分配存储空间, 里面包含Person类的方法列表. 每个方法都对应着一个SEL数据. 根据SEL的值就可以找到方法的地址, 进而调用方法(可以理解为储存了方法的地址).
当对象调用对象方法时, 通过isa指针访问类的方法列表, 是先把test2转换成SEL类型的数据, 再根据SEL的值到类的方法中寻找对应的方法, 再通过方法地址来调用方法.
SEL类型的定义
typedef struct objc_selector *SEL; 可以看到SEL数据也自带星号. 但是它不是对象, 而是一个结构体指针.
SEl数据的创建:
a.使用方法名 SEL sel1 = @selector(test2);
b.使用字符串SEL sel2 = NSSelectorFromString(@"test3:");
注意:
a.如果方法有参数, 方法名的冒号是一定要写的, 否则会报找不到对应的方法(没有参数的方法)
b.这里的方法名可以随意, 可以写一个不存在的方法, 并转换成SEL数据, 并不会报错. 但是如果使用这个SEL给对象发消息的话, 就会报错找不到方法了.
使用SEL变量发送消息
发消息其实发送的就是SEL数据, 所以也可以使用SEL变量给对象发送消息
a.[p test2];
b.[p performSelector:@selector(test2)]; //调用了p的test2方法
(可以写 [p performSeletor:sel1]; // 之前定义了 sel1 就是 test2 对应的SEL数据.)
c.[p performSelector:@selector(test3:) withObject:@"abc"]; // 方法有参数 和[p test3:@"abc"]; 是一样的.
注意:
方法名test3:是有冒号的. 要不就会找不到方法 (unrecognized selector sent to instance).
SEL变量与字符串转换
可以把SEL变量转换成字符串打印, 不能直接打印, 打印出的是它对应方法的方法名
a.NSString *s = NSStringFromSelector(sel2); // 把sel2转换成字符串
NSLog(@"%@", s); // 打印结果是 test3:
b.SEl s =NSSelectorFromString(@"test3");
_cmd
其实每个方法内部都有一个SEL数据 (SEL)_cmd 代表当前方法, 可以用它获得当前方法的名称.
- (void) test2 {
NSString *s = NSStringFromSelector(_cmd); // 把当前方法转换成字符串
NSLog(@"%@", s); // 可以打印出来
} //调用时打印的是这个方法的方法名 test2
如果使用__func__打印, 会打印出方法的全名:
NSLog(@"%s", __func__); // 打印结果是 -[Person test2].