类和对象
面向对象编程,类和对象是两个重要的概念。类是部分对象的抽象,相对而言对象才是实体。例如,日常中接触的人都是人的实例(对象),而这些人都属于人类(类)。
定义类
OC中定义类分为两个步骤:
1. 接口部分:定义该类包含的成员变量和方法
@interface 类名 : 父名 {
//成员变量声明
变量类型 _变量名;
}
//方法声明
方法类型标识 (返回值类型)形参描述:(形参类型)形参名;
@end
成员变量是描述该对象的状态数据,如把一个人当作对象,成员变量可以为姓名,年龄,性别等状态数据。OC中习惯在成员变量名前加“_”。
方法是描述该类的行为,如把一个人当作对象,方法可以是人的吃饭,睡觉,工作等行为。其中由方法类型标识为+或-,+表示该方法是类方法,直接用类名即可调用;-表示该方法是实例方法,必须用对象才能调用。
eg:
@interface MyClass : NSObject {
//成员变量声明
int _count;
id _data;
NSString *_name;
}
//方法声明
- (id)initWithString:(NSString *)aName;
+ (MyClass *)createMyClassWithString:(NSString *)aName;
@end
2. 实现部分:为该类的方法提供实现
@implementation 类名 {
//隐藏成员变量声明
变量类型 _变量名;
}
方法类型标识 (返回值类型)形参描述:(形参类型)形参名 {
//方法体
}
@end
大体结构与接口部分类似,只是加上了方法的实现过程。
实现部分的变量定义为隐藏成员变量,这部分变量只能在当类内访问。
方法体为方法的具体实现过程,与接口部分的方法声明相对应。除了接口部分的方法外,实现部分也可有附加的方法,该部分方法只能在类实现部分使用(供其他方法调用)。
eg:
@implementation MyClass {
int _sex;
}
- (id)initWithString:(NSString *)aName {
//方法体
}
+ (MyClass *)createMyClassString:(NSString *)aName {
//方法体
}
@end
对象的产生和使用
定义完类之后,我们就可以使用类了。使用类有3个步骤:
- 定义变量
- 创建对象
- 调用类方法
定义变量和创建对象的方法如下:
//定义变量
类名 *变量名;
//创建对象
[[类名 alloc] init];
其中alloc为OC的关键字,负责为该类分类内存空间、创建对象;所有对象都继承了NSObject类,有默认的初始化方法init。
我们来看一个具体的程序:
#import <Foundation/Foundation.h>
//接口部分
@interface FKPerson : NSObject {
//成员变量声明
NSString *_name;
int _age;
}
//方法声明
- (void)setName:(NSString *)name andAge:(int)age;
- (void)say:(NSString *)content;
- (NSString *)info;
+ (void)foo;
@end
//实现部分
@implementation FKPerson {
//隐藏的成员变量声明(只能在当前类内访问)
int _testAttr;
}
//方法定义
- (void)setName:(NSString *)n andAge:(int)a {
//方法体:设置姓名年龄
_name = n;
_age = a;
}
- (void)say:(NSString *)content {
//方法体:输出字符
NSLog(@"%@", content);
}
- (NSString *)info {
//方法体:输出姓名年龄
[self test];
return [NSString stringWithFormat:
@"姓名:%@,年龄:%d", _name, _age];
}
- (void)test {
//方法体:输出字符串(仅在实现部分定义)
NSLog(@"只在实现部分定义的test方法");
}
+ (void)foo {
//方法体:输出字符串
NSLog(@"FKPerson类的类方法,通过类名调用");
}
@end
int main() {
@autoreleasepool {
FKPerson *person = [[FKPerson alloc] init];
[person say: @"hello"];
[person setName:@"孙悟空" andAge:500];
NSString *info = [person info];
NSLog(@"person的info信息为:%@", info);
[FKPerson foo];
}
}
self关键字
OC提供了self关键字,它总是指向调用该方法的对象。self关键字最大的作用是能让类中的一个方法调用该类的另一个方法或成员变量。
#import <Foundation/Foundation.h>
@interface Test : NSObject
- (void)jump;
- (void)run;
@end
@implementation Test
- (void)jump {
NSLog(@"jump");
}
- (void)run {
[self jump];
NSLog(@"run");
}
@end
int main() {
@autoreleasepool {
Test *test = [[Test alloc] init];
[test run];
}
}
输出:
我们令run方法依赖于它自己的jump方法,而不是新建一个方法并调用其中的jump方法,减少了多余方法的创建,也更符合逻辑。
当局部变量和成员变量重名时,局部变量会隐藏成员变量。为了在方法中引用成员变量,我们也可以使用self关键字。
#import <Foundation/Foundation.h>
@interface Test : NSObject {
NSString *_name;
int _age;
}
- (void)setName:(NSString *)_name andAge:(int)_age;
- (void)info;
@end
@implementation Test
- (void)setName:(NSString *)_name andAge:(int)_age {
self->_name = _name;
self->_age = _age;
}
- (void)info {
NSLog(@"姓名:%@,年龄:%d", _name, _age);
}
@end
int main() {
@autoreleasepool {
Test *test = [[Test alloc] init];
[test setName:@"顶梁柱" andAge:19];
[test info];
}
}
输出:
setName方法中我们利用 self->成员变量 强行访问并修改了成员变量,最后输出。
当self直接作为对象引用时,程序可以访问这个self引用,也可以把self当作普通方法的返回值。
#import <Foundation/Foundation.h>
@interface ReturnSelf : NSObject {
@public
int _age;
}
- (ReturnSelf *)grow;
@end
@implementation ReturnSelf
- (ReturnSelf *)grow {
_age++;
return self;
}
@end
int main() {
@autoreleasepool {
ReturnSelf *test = [[ReturnSelf alloc] init];
[[[test grow] grow] grow];
NSLog(@"_age:%d", test->_age);
}
}
输出:
grow方法中我们直接把self作为返回值,这样可以多次连续调用这个方法,达到类似递归的效果。
id类型
OC为我们提供了id类型,它可以表示所有对象的类型,即任意类的对象都可以赋值给id类型的变量。
#import <Foundation/Foundation.h>
@interface Test : NSObject
- (void)say:(NSString *)s;
@end
@implementation Test
- (void)say:(NSString *)s {
NSLog(@"%@", s);
}
@end
int main() {
@autoreleasepool {
id test = [[Test alloc] init];
[test say: @"这就是id的用法"];
}
}
输出:
main函数中我们创建了一个Test类的对象,并把它赋值给id类型的test变量,我们可以理解为这个时候“id”相当于“Test *”的作用。
需要注意的是,通过id类型的变量来调用方法时,OC会执行动态绑定,即OC会追踪对象所属的类,在运行时判断对象所属的类、确定需要动态调用的方法,而不是在编译时确定。
形参个数可变的方法
OC中的输出函数NSLog( )可以传入任意多个参数,这就是一个形参个数可变的方法。在定义方法时,我们通过在最后一个形参后添加“,…”,同样可以定义形参个数可变的方法。实现该功能,我们需要认识几个关键字:
- va_list:这是一个类型,用于定义指向可变参数列表的指针变量。
- va_start(参数列表指针, 第一个参数名):这是一个函数,用于指定开始处理可变参数列表的列表,并让指针指向可变参数列表的第一个参数。
- va_end(参数列表指针):这是一个函数,用于结束可变形参,释放指针变量。
- var_arg(参数列表指针, 参数类型):这是一个函数,用于获取指针当前指向的参数值,并将指针移动到下一个参数。
#import <Foundation/Foundation.h>
@interface Test : NSObject
- (void)say:(NSString *)name,...;
@end
@implementation Test
- (void)say:(NSString *)name,... {
//定义指向可变参数列表的指针变量list
va_list list;
if(name) {
NSLog(@"%@", name);
//预处理可变参数列表
//并让指针指向可变参数列表的第一个参数
va_start(list, name);
//获取当前指针指向的参数值给s
//并将指针移向下一个参数
NSString *s = va_arg(list, id);
while(s) {
NSLog(@"%@", s);
s = va_arg(list, id);
}
//结束处理可变形参,释放指针变量
va_end(list);
}
}
@end
int main() {
@autoreleasepool {
id test = [[Test alloc] init];
[test say:@"1", @"2", @"3", @"4"];
}
}
输出:
需要注意的是,个数可变的形参只能处于形参列表的最后,正因为这样,一个方法中最多只能有一个长度可变的形参。
成员变量
OC语言中,根据定义变量的位置不同,可以将变量分为3类:全局变量、局部变量和成员变量。其中全局变量和局部变量是我们熟知的,成员变量则是一个新概念。
成员变量指的是在类接口部分或类实现部分定义的变量,OC的成员变量都是实例变量。
实例变量是从一个实例被创建开始存在,到这个实例被完全销毁也被释放的变量。实例变量的作用域与对应实例的生存范围相同。
在定义成员变量时无须初始化,基本类型的成员变量默认被初始化为0;指针类型的成员变量默认被初始化为nil。
模拟类变量
类变量是指附属于一个类的变量,通常为静态变量,而实例变量通常是动态的。
类变量和实例变量的区别在于:类变量是所有对象共有,其中一个对象将它值改变,其他对象得到的就是改变后的结果;而实例变量则属对象私有,某一个对象将其值改变,不影响其他对象。类变量是依附于类的公有变量,实例变量是依附于对象的私有变量。
OC虽然不像Java支持类变量,但是可以模拟类变量。
#import <Foundation/Foundation.h>
@interface Test : NSObject
+ (NSString *)nation;
+ (void)setNation:(NSString *)newNation;
@end
static NSString *nation = nil;
@implementation Test
+ (NSString *)nation {
return nation;
}
+ (void)setNation:(NSString *)newNation {
//isEqualToString是判断字符串是否相等的方法
if(![nation isEqualToString:newNation]) {
nation = newNation;
}
}
@end
int main() {
@autoreleasepool {
[Test setNation:@"中国"];
NSLog(@"Test的类变量nation为:%@", [Test nation]);
}
}
输出:
单例模式
有时候,我们在整个程序中只需要一个类的一个实例,多次创建实例再回收反而会降低系统性能。如果一个类始终只能创建一个实例,这个类被称为单例类。单例类可通过static全局变量实现。
#import <Foundation/Foundation.h>
@interface Test : NSObject
+ (id)instance;
@end
@implementation Test
static id instance = nil;
+ (id)instance {
if (!instance) {
instance = [[super alloc] init];
}
return instance;
}
@end
int main() {
@autoreleasepool {
NSLog(@"%d", [Test instance] == [Test instance]);
}
}
输出:
输出1代表了两次两次产生的对象是同一个对象,即该类仅有一个实例。
隐藏和封装
面向对象有三大特征:封装、继承和多态。其中封装指的是将对象信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息,而只能通过该类所提供的方法来实现对内部信息的访问及操作。
对一个类或对象实现良好的封装,有以下优点:
- 隐藏类的实现细节
- 限制使用者访问数据的方法,避免不合理访问
- 可进行数据检查,保证对象信息的完整性
- 便于修改,提高代码可维护性
为了实现良好的封装,需要从两方面考虑:
- 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问
- 将方法暴露出来,用方法保证这些成员变量进行的访问和操作是安全的。
这两方面都需要使用访问控制符来实现。
访问控制符
OC提供了4个访问控制符:@private、@package、@protected和@public,分别代表了四个访问控制级别。
- @private(当前类访问权限):成员变量只能在当前类内部被访问,相当于完全隐藏。在类的实现部分定义的成员变量默认为这种权限。
- @package(映像访问权限):成员变量可以在当前类以及当前类实现的同一个映像的任意地方访问,相当于部分隐藏。
- @protected(子类访问权限):成员变量可以在当前类、当前类的子类的任意地方访问,相当于部分暴露。
- @public(公共访问权限):成员变量可以在任意地方访问,相当于完全暴露。
@private | @package | @protected | @public | |
---|---|---|---|---|
同一个类中 | ✓ | ✓ | ✓ | ✓ |
同一个映像中 | ✓ | ✓ | ||
子类中 | ✓ | ✓ | ||
全局范围内 | ✓ |
可以看出,访问控制符用于控制类的成员变量是否可以被其他类访问。而对于局部变量而言,其作用域就是它所在的方法,不可能被其他类来访问,也就不能用访问控制符来修饰。
#import <Foundation/Foundation.h>
@interface Test : NSObject {
@private
NSString *_name;
int _age;
}
- (void)setName:(NSString *)name;
- (NSString *)name;
- (void)setAge:(int)age;
-(int)age;
@end
@implementation Test
static id instance = nil;
- (void)setName:(NSString *)name {
if([name length] > 6 || [name length] < 2) {
NSLog(@"名称长度不合法!");
return;
}else {
_name = name;
}
}
- (NSString *)name {
return _name;
}
- (void)setAge:(int)age {
if(age > 100 || age < 10) {
NSLog(@"年龄不合法!");
return;
}else {
_age = age;
}
}
-(int)age {
return _age;
}
@end
int main() {
@autoreleasepool {
Test *p = [[Test alloc] init];
//p->_age = 1000;
[p setAge:1000];
NSLog(@"age未成功设置时:%d", [p age]);
[p setAge:30];
NSLog(@"age成功设置后:%d", [p age]);
[p setName:@"顶梁柱"];
NSLog(@"name成功设置后:%@", [p name]);
}
}
输出:
当取消main函数中注释时,编译器会报错:
因为_age变量为private变量,在当前类的外部无法访问。
一个类通常为一个小模块,我们只公开必须让外界知道的内容,隐藏其他内容。设计程序时,要尽量避免一个模块直接操作和访问另一个模块的数据,做到高内聚;仅暴露少量方法给外部使用,做到低耦合。
使用访问控制符的基本原则:
- 类里绝大部分成员变量用@private限制,将辅助其他方法实现的工具方法定义在类实现部分,以隐藏于类中。
- 如果某个子类作为其他类的父类,该类里包含成员变量希望被子类访问,可以考虑使用@protected限制。
- 希望给其他类自由调用的方法应先在类接口部分定义,然后在类实现部分实现。
关于@package
@package限制的成员变量“可以在当前类以及当前类实现的同一个映像的任意地方访问”,这里的“同一映像”指的是编译后生成的同一个框架或同一个执行文件。
如果我们要开发一个基础的框架,考虑该框架的其他类、函数也需要直接访问该成员变量,但又不希望 其他外部程序访问该成员变量,就可以使用@package。
合成存取方法
为实现成员变量的存取,我们可以为每个成员变量提供setter和getter方法。但当成员变量很多的时候,这种做法过于繁琐。因此,OC为我们提供了合成存取方法,它分为两步:
- 在类接口部分使用@property定义属性
- 在类实现部分用@synthesize声明该属性
完成这两步之后,不仅会合成成对的setter和getter方法,还会自动在类实现部分定义一个与getter方法同名的成员变量。如果某一个类定义了一个成员变量,并提供了相应的setter、getter方法,那么可以称作定义了一个属性。
#import <Foundation/Foundation.h>
@interface Test : NSObject
//定义三个property
@property (nonatomic)NSString *name;
@property NSString *pass;
@property NSDate *birth;
@end
@implementation Test
//为三个property合成setter和getter方法
//指定name底层对应的成员变量名为_name
@synthesize name = _name;
@synthesize pass;
@synthesize birth;
//实现自定义的setName方法
- (void)setName:(NSString *)name {
self->_name = [NSString stringWithFormat:@"+++%@", name];
}
@end
int main() {
@autoreleasepool {
Test *test = [[Test alloc] init];
[test setName:@"admin"];
[test setPass:@"1234"];
[test setBirth:[NSDate date]];
NSLog(@"管理员账号:%@,密码:%@,生日:%@", [test name], [test pass], [test birth]);
}
}
输出:
当使用@property定义property时,还可以在@property和类型中用括号添加一些额外的指示符:
- assign:该指示符指定对属性只是进行简单的赋值,不更改对所赋值的引用计数。这个指示符主要适用于NSInteger等基础类型,以及int、double、结构体等C语言的数据类型。(引用计数是OC内存回收的概念:当一个对象的引用计数大于0时,表明该对象还不应该被回收;assign适用的指定对象都不存在回收问题,故用assign)
- atomic(nonatomic):指定合成的存取方法是否为原子操作。
- copy:使用copy指示符时,当调用setter方法对成员变量赋值时,会被赋值对象复制一个副本,再将该副本赋值给成员变量。copy指示符会将原成员变量所引用对象的引用计数减1。当成员变量的类型是可变类型,或其子类是可变类型时,被赋值的对象有可能在赋值之后被修改;如果程序不需要这种修改影响setter方法设置的成员变量的值,此时就可以考虑使用copy指示符。
#import <Foundation/Foundation.h>
@interface Test : NSObject
//使用@property定义一个property
@property (nonatomic)NSString *name;
//@property (nonatomic, copy)NSString *name;
@end
@implementation Test
@synthesize name;
@end
int main() {
@autoreleasepool {
Test *test = [[Test alloc] init];
NSMutableString *str = [NSMutableString stringWithString:@"iOS"];
[test setName:str];
NSLog(@"test的name为:%@", [test name]);
[str appendString:@"真难"];
NSLog(@"test的name为:%@", [test name]);
}
}
输出:
main函数中name变量是一个NSString类型,NSString类型有一个NSMutableString子类。当我们把一个NSMutableString对象复制给test的name时,由于定义name未使用copy指示符,NSMutableString对象可能被修改,且这样的修改会影响Test的name属性值。
当定义name属性时改用注释中增加copy指示符的方式,输出如下:
原因是当程序执行[book setName:str]时,程序会将str指向的NSMutableString对象复制一个副本,再将副本作为setName的参数值,因此通过修改str修改NSMutableString时,Test的name属性并不会改变。
- getter、setter:为合成的getter、setter方法自定义方法名。
用法:getter = 方法名 setter = 方法名: (setter要带参数,故有冒号)
#import <Foundation/Foundation.h>
@interface Test : NSObject
@property (assign, nonatomic, getter = get, setter = set:)int price;
@end
@implementation Test
@synthesize price;
@end
int main() {
@autoreleasepool {
Test *test = [[Test alloc] init];
[test set:30];
NSLog(@"price:%d", [test get]);
}
}
输出:
- readonly、readwrite:readonly指示系统只合成getter方法,不再合成setter方法;readwrite是默认值,让系统合成setter和getter方法。
- retain:使用retain定义的属性,当被某个对象赋值后,该属性原来所引用对象的引用计数减1,被赋值对象的引用计数加1。