OC类与对象
本篇是对上一篇的内容的继续学习。从单例模式开始继续学习
文章目录
- 单例模式
- 定义
- 应用场景
- 特点
- 单例模式的创建
- 隐藏与封装
- 理解什么是封装
- 目的
- 访问控制符
- 合成存取方法
- 特性的指示符
- 点语法访问属性
- 对象初始化
- 便利的初始化方法
- 类的继承
- 特点
- 语法格式
- 重写父类方法
- super关键字
- 类的多态
- 指针变量的强制类型转化
- 判断实际指针的类型
- 小结
单例模式
单例模式是因为在某些时候,程序多次创建这一个类的对象没有实际上的意义,那么我们就只用在程序运行的时候只初始化一次,自然就产生了我们的一个单例模式。
定义
如果一个类始终只能创建一个实例,则这个类被称为单例类。在程序中,单例类只在程序中初始化一次,所以单例类是储存在去全局区域,在编译时分配内存,只要程序还在运行就会一直占用内存,只有在程序结束的时候释放这部分内存。
应用场景
我们只要保证全局有且只有一份就可以,一般在程序中,经常调用的类,如工具类、公共跳转类等都会采用单例模式。
特点
- 单例是一个类,创建出来的对象就是单例对象
- 单例对象使用类方法创建
- 单例一旦被创建出来,一直到程序结束才释放,就是只初始化一次
- 不用程序员管理,直到程序结束就自动释放
单例模式的创建
因为单例模式只能创建一次,那么我们就把它存储在我们的一个静态区,那么就要用到我们的一个关键字static,这个关键字可以让我们的一个变量存储到静态区,下面将给出代码实现。
接口部分
@interface FKSingletion : NSObject {
;
}
+ (id) instance; //设计一个类方法来创建一个单例模式
@end
实现部分
static id instance = nil;
@implementation FKSingletion
+ (id) instance {
if(!instance) {
instance = [[super alloc] init];
}
return instance;
}
@end
这样我们就实现了一个单例模式的创建,但是实际上这个方法还有问题就是我们在通过[[FKSingletion alloc] init];
这个语句创建的时候,还是会出现一个问题,就是我们还是能创建一个新的对象,这样就不符合我们对于单例模式的一个定义。这时候我们可能需要对之前的进行一个改进。
笔者查阅博客发现这和我们的alloc会执行一个allocWithZone,如果我们只想分配一次内存就要重写这个方法allocWithZone。
接口部分
+ (id) allocWithZone:(struct _NSZone *)zone;
+ (id) instance;
实现部分
+ (id) instance {
return [[self alloc] init];
}
+ (id) allocWithZone:(struct _NSZone *)zone {
if (!instance) {
instance = [super allocWithZone:zone]; //重写这个方法
}
return instance;
}
我们这时候在主函数中执行如下代码
FKSingletion* person = [FKSingletion instance];
FKSingletion* p2 = [[FKSingletion alloc] init];
NSLog(@"%p %p", person, p2); //打印出两个地址
我们发现两个地址是一样的,这里我们就实现了一个意义上的一个单例。(实际上还要修改一系列函数保证一个单例例如:copyWithZone,mutableCopyWithZone)但笔者对于这部分还没有学习,之后还会进行一个补充。
隐藏与封装
前面我们讲了直接通过对象访问实例变量的情况,但是这样实际上会引发一系列问题,比如说我们如果直接执行这条语句FKpreson->_age = 1000
语法上没有问题,但是绝对不符合逻辑,因此我们需要把这个东西隐藏在对象内部,不允许外部程序访问对象的内部信息。
理解什么是封装
封装是面向对象语言编程对客观世界的模拟,不允许外部程序访问对象的一个内部信息,而是通过这个类的方法来实现对于内部信息的操作和访问。
目的
- 隐藏类的实现细节
- 使用者只能通过预先的方法来访问数据,从而可以在方法内计入逻辑,防止不合理访问。
- 可进行数据检查,保证信息的完整性
- 便于修改。
封装的实际上的含义就是:把该隐藏的隐藏起来,把该暴露的暴露出来。
访问控制符
OC有四个访问控制符:@private, @package,@protected,@public。
- @private:这个访问控制符下的成员变量只能在这个类的内部访问。这个用来彻底隐藏成员变量
- @package:这个访问控制符,可以在当前类和类的同一映像中访问(就是编译后的同一框架或同一个执行文件),这一般就是考虑到,在开发一个框架中,其他类或者其他函数也要用到这个成员变量,我们就要考虑使用这个访问控制符。这些类、函数就都在一个映像中(注意这里的函数包括我们的主函数)。
- @protect:这个访问控制符号只允许类和子类进行一个访问
- @public:这个访问控制符号可以让成员变量在任何地方进行一个访问。
接口部分
@interface Fkbaskerball : NSObject {
@private
int _value;
int _time;
}
- (void) setValue : (int) value;
- (void) setTime : (int) time;
- (int) value;
- (int) time;
@end
实现部分
@implementation Fkbaskerball {
}
- (void) setTime: (int)time {
self->_time = time;
}
- (void) setValue:(int)value {
self->_value = value;
}
- (int) value {
return self->_value;
}
-(int) time {
return self->_time;
}
@end
在定义上面这个类的时候我们要注意我们通过一个setter方法和一个getter方法来实现了对于这个类中成员变量的一个操作和访问,在这个类之外只能通过这两个方法来访问这两个成员变量。
当我们在主函数中想访问这两个变量的时候,因为他是@private的性质所以他会报错。
tips
这里面讲了我们对于使用这访问控制符的一个使用的基本原则
合成存取方法
- 在新的OC中间可以自己生成一个setter和getter方法,我们则需要使用**@property指令来定义属性。使用这个指令的时候我们无须将他直接放在@interface和@end**之间进行一个定义,这个指示符放在属性定义的前面。
- 在类的实现部分使用一个**@synthesize**指令来声明该属性。
注意:我们还是可以重写setName方法。
接口部分
@interface FKSingletion : NSObject {
}
@property int age;
@end
这个部分的代码实际上就是
@interface FKSingletion : NSObject {
}
- (void) setAge: (int) newAge;
- (int) age;
@end
实现部分
@implementation FKSingletion
@synthesize age = _age; //将成员变量写成_age
@end
这部分代码实际上就是
- (void)setAge:(int)age
{
_age = age;
}
- (int)age
{
return _age
}
特性的指示符
- assign:该指示符指定对属性只是进行一个简单的赋值,不更改对所赋的值的引用计数。
- atmoic(nonatomic)指定合成的存取方法是否为一个原子操作。
- copy:如果使用copy指示符,当调用一个setter方法对成员变量赋值的时候,会将被赋值的对象复制一个副本,再将副本值赋给成员变量。
我们来重点理解一下copy这一个指示符,我们通过一个代码进行一个分析。
下面是接口部分与实现部分:
@interface Fkbaskerball : NSObject {
;
}
@property (nonatomic) NSString* name;
@end
@implementation Fkbaskerball {
}
@synthesize name;
@end
然后我们通过调用主函数使用这个方法。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Fkbaskerball* f1 = [[Fkbaskerball alloc] init];
NSMutableString* str = [NSMutableString stringWithString:@"blackman"]; //NSMutableString是子类可以被修改的字符串类型。
[f1 setName: str];
NSLog(@"他叫:%@", [f1 name]);
[str appendString:@" is good"];
NSLog(@"%@", [f1 name]);
}
return 0;
}
打印出来的结果是
我们这里就可以发现我们在修改str值的时候同时修改了FKbasketball的_name值。
如果我们这一次将@property (nonatomic) NSString* name;
修改为@property (nonatomic, copy) NSString* name;
时候,发现结果变成了这样。这就意味着我们name值没有因为str的修改而修改。
strong和weak这两个指示符表示对于被赋值对象持有强引用还是弱引用,他们两个决定对象是否被自动回收
点语法访问属性
如果每一次设置属性都要用setter或者是getter方法的话就会非常麻烦,所以现在设计了点语法的形式来访问我们的属性,或者对我们的属性进行一个赋值,或者通过一个点语法访问。主要取决于我们的点语法的一个位置。(在左侧就是一个赋值,在右侧就是一个访问)。
f1.name = str; //这个是赋值语句
NSLog(@"他叫:%@", f1.name); //这个是访问语句
对象初始化
对象初始化调用alloc方法的时候,系统会为我们所有的实例变量分配内存空间,将每一个实例变量的内存空间变成0,同时对应类型为对应的一个空值,仅仅分配内存空间对象还是不能使用的,还需要进行一个初始化,需要init方法才可以使用它。
我们在对象初始化的过程中如果使用了[类名 alloc]
语句来创建一个对象的话,那没我们就没有对对象进行一个初始化,因此调用这个方法会出问题,在实际开发中间我们可以自己设计一个自己的init方法
- (id) init {
if (self = [super init]) { //这一步的意思是我们先对self进行一个初始化,如果初始化失败那么self就是nil会退出,如果初始化成功那么就一位置我们可以按下面的步骤进行一个自定义的初始化
self.name = @"messi";
self.time = 30;
}
return self;
}
便利的初始化方法
因为我们从上面的方法认识到这并不可以根据参数进行一个动态初始化,我们可以自定义一些初始化方法。我们以initXXX的方式进行一个传入自己参数进行一个初始化。
- (id) init {
if (self = [super init]) {
self.name = @"messi";
self.time = 35;
self.value = 1000;
}
return self;
}
- (id) initWithname: (NSString*) name andTime : (int) time {
if (self = [super init]) {
self.name = name;
self.time = time;
}
return self;
}
- (id) initWithname: (NSString *) name andTime : (int) time andValue : (int) value {
if (self = [self initWithname: name andTime: value]) {
self.value = value;
}
return self;
}
这里我们有三种不同的调用方式去调用这些代码,我们这里包含了多个初始化方法,我们让其中一个初始化方法执行体中完全包含另一个初始化方法的执行体,我们在第三个方法中间调用初始化方法2,这样可以让我们更好的实现一个代码复用。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Fkbaskerball* f1 = [[Fkbaskerball alloc] init];
NSLog(@"%@ %d %d", f1.name, f1.value, f1.time);
Fkbaskerball* f2 = [[Fkbaskerball alloc] initWithname:@"neyal" andTime: 30];
NSLog(@"%@ %d %d", f2.name, f2.value, f2.time);
Fkbaskerball* f3 = [[Fkbaskerball alloc] initWithname:@"manba" andTime: 40 andValue: 1500];
NSLog(@"%@ %d %d", f3.name, f3.value, f3.time);
}
return 0;
}
打印结果为:
可见它实现了一个自定义的初始化。
类的继承
继承是面向对象的三大特性之一,也是实现一个软件复用的重要手段,OC的继承具有单继承的特点,每一个子类只有一个直接父类
特点
父类和子类是一种一般和特殊的关系,因为子类是一种特殊的父类,就好比水果和苹果一样,两个都是抽象的概念但是还是有很大的差异,苹果是属于水果的。
语法格式
@interface SubClass : SuperClass {
}
@end
这表明我们的subClass继承了SuperClass类,继承的含义可能更适合用扩展来描述。我们可以说是Apple类拓展了Fruit类。当子类扩展父类的时候,子类可以继承父类的如下东西:
- 全部成员变量
- 全部方法(包括初始化方法)
下面我会给出一个例子
@interface Fruit : NSObject {
}
@property (nonatomic) double weight;
@end
@interface Apple : Fruit {
;
}
@end
//上面是接口部分下面是实现部分
@implementation Fruit {
;
}
@synthesize weight;
@end
@implementation Apple {
;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
Apple* greenApple = [[Apple alloc] init];
greenApple.weight = 15.87; //调用setter方法
NSLog(@"%lg", greenApple.weight);
}
return 0;
}
我们可以看到实际上我们上面的Apple
类实际上并没有任何成员变量和方法,但是我们发现我们主函数却可以调用setter方法。这就说明了我们的一个继承的特性。
重写父类方法
我们其实在上面就已经重写过父类的方法,因为我们之前重写过init
方法,因为我们所有的类都是NSObject
的一个子类,从上面我们就发现了如果重写一个方法的话,一定要做到的要求是:方法签名关键字要完全相同,也就是方法名和方法签名中的形参标签都需要完全相同,否则就不算是方法重写
super关键字
其实在之前我们就多次调用过这个关键字,super用于限定该对象调用它从父类继承得到的属性或者方法。但他正如self一样不能出现在类方法里,super也只能调用实例方法,因为类的方法的调用者是类本身。
@interface FKn : NSObject {
@private
int _a;
}
@end
//这里定义了一个父类一个子类
@interface FKtian : FKn {
int _a;
}
@end
上面的这种写法是存在问题的,因为OC有一个要求就是:无论父类接口部分的成员变量使用何种访问控制符限制,子类接口部分定义的成员变量都不允许与父类接口部分定义的成员变量重名,但是我们又要注意在类的实现部分定义的成员变量将被限制在该类的内部,子类无论是接口部分还是实现部分定义的成员变量都可以与父类实现部分定义的成员变量同名。
下面我给出一个例子,这里给出了我们强制调用父类的a属性,我们可以通过这种方式来访问到父类中被隐藏的成员变量
@implementation nnnn
@synthesize a = _a;
- (id) init {
if (self = [super init]) {
self->_a = 6;
}
return self;
}
@end
@implementation nextnnnn {
int _a;
}
- (id) init {
if (self = [super init]) {
self->_a = 7;
}
return self;
}
- (void) answer { //通过这个来打印当前类中的成员变量
NSLog(@"父类中的变量%d", super.a);
NSLog(@"子类中的成员变量%d", self->_a);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
nextnnnn* p1 = [[nextnnnn alloc] init];
[p1 answer];
}
return 0;
}
从上面的运行结果可以看出,程序通过super关键字强制指定调用父类的a属性,通过这种方式获取(getter方法返回值)
类的多态
OC的指针类型有两个:一个是编译时候的类型,一个是运行时的类型,编译的类型由声明该变量时使用的那类型来决定,如果编译的时候和运行的时候类型不一样,就有可能出现所谓的多态。
我们先定义一下父类和子类
下面我只给出接口部分
@implementation FKBase //父类
- (void) base {
NSLog(@"父类的方法");
}
- (void) test {
NSLog(@"父类将被覆盖的方法");
}
@end
@implementation FKsub //子类部分
- (void) test {
NSLog(@"子类覆盖父类的方法");
}
- (void) sub {
NSLog(@"子类自己的方法");
}
@end
然后我们通过调用主函数来分辨它所调用的方法。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
FKBase* ba = [[FKBase alloc] init];
[ba test];
[ba base];
FKsub* bb = [[FKsub alloc] init];
[bb test];
[bb sub];
FKBase* bc = [[FKsub alloc] init];
[bc test];
[bc base];
}
return 0;
}
输出结果是如下图所示。
这就说明了一个问题就是将子类对象赋给父类指针对象那个,就出现了一个多态。当我们将一个子类的对象赋给父类指针变量,他的行为方法总是表现出子类方法的行为特征,而不是父类的行为特征,他也只能调用父类的方法,但是父类方法呈现的却是一个子类的行为。
所以我们可以通过id来解决编译时候类型检查问题
指针变量的强制类型转化
除了id类型的变量之外,指针变量只能调用它编译的时候类型的方法,而不能调用它运行时类型的方法,如果需要让这个指针变量调用它运行的时候的类型的方法,则必须要强制类型转换成运行时候的类型,强制类型转换需要借助类型转换符。
tips:强制类型转换只改变了编译时期的类型,而没有改变它实际所指向的类型,如果调用编译时期的类型的话会报错。
子类对象赋给父类对象指针变量的时候,被称为向上转型,这种转型总是可以成功的。上面报错的主要原因是,date的编译时的编译类型虽然是NSDate,但是他实际指向的对象时NSString,因此调用date方法的方法时,虽然编译可以通过,但是运行时会引起错误。
判断实际指针的类型
- -(BOOL) isKindOfClass:clazz:判断该对象是否为clazz或者他的子类的实例
- -(BOOL)isSubclassOfClass:clazz:判断该对象是否为clazz的子类的实例
小结
- 单例模式原理在一个程序中只需要一个实例的环境下运用,那么我们在这种情况下就要采用一个单例模式来实现,我们通过一个static和一个类方法来实现我们的单例模式,核心代码部分就是
if (!instance)
和instance = [[super alloc] init]
。 - 隐藏和封装部分要注意不同的访问控制符对应的不同范围以及一些特殊指示符
- 对象初始化方面我们可以重写一个初始化函数,以及初始化对象会给所有值赋值为0或者是nil。
- 类的继承:我们可以理解为子类是父类的一个拓展,子类可以重写父类的方法,子类的接口部分的成员变量不能和父类接口部分的成员变量相同
- 类的多态:我们重点理解了是编译时期的变量类型和实际运行时的编译类型不同会导致多态的出现,指针变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行其运行时类型所具有的方法。