【Objective-C】面向对象(上)

OC是面向对象的程序设计语言,它提供了定义类、成员变量和方法的基本功能。

类拥有描述客观世界中某一类对象的共同特征,而对象则是类的具体存在。

面向对象三大基本特征:封装、继承和多态。

类和对象

类可被认为是一种自定义的数据类型,使用它可以定义变量,所有使用类定义的变量都是指针类型的变量,它们将会指向该类的对象。

定义类

类(class)和对象(ocject,也称为实例,instance),其中类是某一批对象的抽象,可以把类理解成某种概念,对象才是一个具体存在的实体。

定义类需要分为两个步骤:

  1. 接口部分:定义该类包含的成员变量和方法。
  2. 实现部分:为该类的方法提供实现。
定义接口部分

语法如下:

@interface ClassName : NSObject  //@interface用于声明定义类的接口部分
{  //紧跟的一对花括号用于声明该类的成员变量
  int _count;
  id _data;
  NSString* _name;
}

- (id)initWithString: (NSString*)aName;  //花括号后的部分用于声明该类的方法
+ (ClassName*)createMyClassWithString: (NSString*)aName;
@end  //@end表明定义结束

**成员变量:**用于描述该类的对象的状态数据。OC建议成员变量名以下划线开头。

**方法:**用于描述该类的行为。

声明方法

语法如下:

在这里插入图片描述

**方法类型标识符:**该标识符要么是+,要么是-,其中,+代表该方法是类方法,直接用类名即可调用;-代表该方法是实例方法,必须用对象才能调用。

**方法返回值类型:**与C语言类似。

**方法签名关键字:**OC的方法签名关键字由方法名、形参标签和冒号组成。方法名通常以动词开头。除了第一个形参外,OC建议为后面每个形参都指定一个形参标签,该形参标签可以很好地说明该形参的作用——虽然OC允许省略形参标签,但这并不是一种好的编程习惯,因此建议保留形参标签。

定义实现部分

语法如下:

@implementation ClassName
{
  int _count;
  id data;
  NSString* _name;
}

- (id)initWithString: (NSString*)aName
{
  // 方法体
}

+ (ClassName*)createMyClassWithString: (NSString*)aName
{
  // 方法体
}
@end

成员变量类实现部分也可以声明自己的成员变量,但这些成员变量只能在当前类内访问。因此,在类实现部分声明成员变量相当于定义隐藏变量。

方法类实现部分除了实现类接口部分定义的方法之外,也可提供额外的方法定义——这些没有在接口部分定义,只是在实现部分定义的方法,将只能在类实现部分使用。方法体里多条可执行语句之间由严格的执行顺序。

THGPerson类实例分析

接口部分

#import <Foundation/Foundation.h>

@interface THGPerson : NSObject
{
  NSString* _name;
  int _age;
}

- (void)setName: (NSString*)name andAge: (int)age;
- (void)say: (NSString*)content;
- (NSString*) info;  //不带形参的info方法
+ (void) foo;  //定义一个类方法
@end

根据习惯,Objective-C把接口部分和实现部分分别使用两个源文件保存(大家约定俗成的规则),其中接口部分的源文件通常命名为*.h文件,实现部分的源文件通常命名为*.m文件。

实现部分

@import "THGPerson.h"

@implementation THGPerson
{
  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(@"--只能在实现部分使用的方法--");
}

+ (void) foo
{
  NSLog(@"THGPerson类的类方法,通过类名调用");
}
@end
  • 实现info方法时使用了NSString的**stringWithFormat:**类方法(直接通过类名调用的类方法),该方法的作用时将多个变量“镶嵌”到字符串中输出。
  • setName方法与类接口部分定义的方法签名略有所不同——只是方法形参名不同,这是允许的。对于Objective-C的方法而言,方法的形参名仅仅相当于一个占位符,因此,该方法声明中的形参名与方法接口部分的形参名完全可以不同。
  • 实际上,如果让setName:andAge:方法的形参名依然保持_name_age,反而更加麻烦,因为这个形参名和接口部分定义的成员变量重名,这样局部变量将会隐藏成员变量。

对象的产生和使用

定义类之后,可从如下三方面来使用类:

  1. 定义变量
  2. 创建对象
  3. 调用类方法

定义变量的语法:

类名* 变量名;

创建对象的语法:

[[类名 alloc] 初始化方法];

alloc时OC的关键字,该关键字负责为该类分配空间、创建对象。除此,还需要调用初始化方法对该实例执行初始化。由于所有的对象都继承了NSObject类,因此所有的类都有一个默认的初始化方法:init

OC也支持使用new来创建对象:

[类名 new];

这种写法基本等同于[[类名 alloc] init];,但使用较少。

调用方法的语法:

[调用者 方法名:参数 形参标签:参数值…];

虽然OC允许调用方法传入参数时,省略形参标签,但省略形参标签的做法会降低程序的可读性,因此不建议省略形参标签

如果访问权限允许,OC允许直接通过对象来访问成员变量。通过对象访问成员变量的语法:

对象->成员变量名

下面以THGPerson类的用法为实例:

THGPerson* person;  //定义THGPerson*类型变量
person = [[THGPerson alloc] init];  //创建THGPerson对象,赋给person

// THGPerson person = [[THGPerson alloc] init];

[person say: @"Hello, iOS!"];
[person setName: @"Kevince" andAge: 20];

NSString* info = [person info];
NSLog(@"person的info信息为:%@", info);

[THGPerson foo];  //通过类名来调用类方法

大部分时候,定义一个类就是为了重复创建该类的实例。

对象和指针

在上面的实例中,有这样一行代码:THGPerson* person = [[THGPerson alloc] init];这行代码实际上产生了两个东西:一个是person变量,一个是THGPerson对象。

从THGPerson类定义来看,THGPerson对象应包含三个成员变量(两个可以暴露的成员变量和一个被隐藏的成员变量),而成员变量是需要内存来存储的。因此,当创建了THGPerson对象时,必须要有对应的内存来存储THGPeson对象的成员变量。

下图清晰地显示了THGPerson对象在内存中的存储示意图。

在这里插入图片描述

从图中可以看出,THGPerson对象由多块内存组成,不同的内存块分别存储了THGPerson对象不同的成员变量。当把这个THGPerson对象赋给一个THGPerson*变量时,系统是如何让处理的呢?

从本质上说,类也是一种指针类型的变量,因此,程序中定义的THGPerson*类型只是存放一个地址值,ta被保存在该main()函数的动态存储区,ta指向实际的对象,而真正的THGPerson对象则存放在堆(heap)内存中

下图显示了将THGPerson对象赋给指针变量的示意图。

在这里插入图片描述

main()函数的动态存储区保存的指针变量并未真正存储对象里的成员变量数据,而指针变量仅仅是指向该对象。

当一个对象被创建成功以后,这个对象将保存在堆内存中,OC不允许直接访问堆内存中的对象,只能通过该对象的指针变量来访问该对象。

从上图可以看出,person指针变量本身只存储了一个地址值,并未包含任何实际的数据,但ta指向实际的THGPerson对象,当调用person指针变量的成员变量和方法时,实际上是访问person所指对象的成员变量和方法。

堆内存里的对象可以有很多指针,即多个指针变量指向同一个对象:

THGPerson* p2 = person;

在这行代码中,把person变量的值赋给p2变量,也就是将person变量保存的地址值赋给p2变量,这样p2变量和person将指向堆内存里的同一个THGPerson对象。不管是访问p2变量的成员变量和方法,还是访问person变量的成员变量和方法,ta们实际上是访问同一个THGPerson对象的成员变量和方法,将会返回相同的访问结果。

如果堆内存里的对象没有任何变量指向该对象,那么程序将无法再访问该对象,OC要求程序员释放该对象所占用的内存,否则就会造成内存泄漏。

如果程序不回收这些内存,OC会以为这些内存依然被“占用”着,以后将不会分配这些内存,这就会造成内存泄漏。长此以往,程序可用的内存越来越少,程序性能自然也就越来越差了。

Xcode4.2引入了自动引用计数(Automatic Reference Counting, ARC),这个新特性可以很好地解决垃圾回收问题。

self关键字

OC提供了一个self关键字,self关键字总是指向该方法的调用者(对象或类),当self出现在实例方法中时,self代表调用该方法的对象;当self出现在类方法中时,self代表调用该方法的

self关键字最大的作用是让类中的一个方法访问该类的另一个方法或成员变量。

先来看看下面的实例,假设定义了一个THGDog类,这个THGDog对象的run方法需要调用ta的jump方法:

#import <Foundation/Foundation.h>

@interface THGDog : NSObject
  - (void)jump;
  - (void)run;
@end
#import "THGDog.h"
@implementation THGDog
  - (void)jump
  {
  NSLog(@"正在执行jump方法");
  }

  - (void)run
  {
    THGDog* d = [[THGDog alloc] init];
    [d jump];
    NSLog(@"正在执行run方法");
  }
@end

使用这种方式来定义这个THGDog类,确实可以实现在run方法中调用jump方法,那么这种做法是否够好呢?来看看下面THGDog对象的创建以及该对象run方法的调用。

#import <Foundation/Foundation.h>
#import "THGDog.h"

int main(int argc, char* argv[])
{
  @autoreleasepool {
    THGDog* dog = [[THGDog alloc] init];
    [dog run];
  }
}

在上面的程序中,一共产生了两个THGDog对象。这里产生了两个问题:

  1. 在run方法中调用jump方法时是否一定需要一个THGDog对象?
  2. 是否一定需要重新创建一个THGDog对象?

第一个问题的答案是肯定的,因为jump方法是一个实例方法,因此必须使用对象来调用。第二个问题的答案是否定的,因为当程序调用run方法时,一定会提供一个THGDog对象,这样就可以直接使用这个已经存在的THGDog对象,而无须重新创建新的THGDog对象。

因此需要在run方法中获得调用该方法的对象,通过self关键字可以满足这个要求。

self总是代表当前方法的调用者,当这个方法被调用时,ta所代表的对象才被确定下来:谁在调用该方法,self就代表谁

将前面的THGDog类实现部分改写为如下形式会更加合适:

#import "THGDog.h"

@implementation THGDog
  - (void)jump
  {
    NSLog(@"正在执行jump方法");
  }
  
  - (void)run
  {
    [self jump];
    NSLog(@"正在执行run方法");
  }
@end

上面的代码更符合实际情形:当一个THGDog对象调用run方法时,run方法需要依赖ta自己的jump方法。

在局部变量和成员变量重名的情况下,局部变量会隐藏成员变量。为了在方法中强行引用成员变量,也可以使用self关键字进行区分。看以下示例:

#import <Foundation/Foundation.h>

@interface THGWolf : NSObject
{
  NSString* _name;
  int _age;
}

- (void)setName: (NSString*)_name andAge: (int) _age;  //重名
- (void) info;
@end
  
@implementation THGWolf
 - (void)setName: (NSString*)_name andAge: (int)_age
{
  //当局部变量隐藏成员变量时
  //可用self代表调用该方法的对象,这样就可以为调用该方法的对象的成员变量赋值(得以访问)
  self->_name = name;
  self->_age = _age;
}

- (void)info
{
  NSLog(@"我的名字时%@,年龄时%d岁", _name, _age);
}
@end
  
int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGWolf* w = [[THGWolf alloc] init];
    [w setName: @"灰太狼" andAge: 8];
    [w info];
  }
}

在上面程序的setName:andAge:方法中,由于_name、 _age形参隐藏了 _name、 _age成员变量,因此编译器会提示警告,由于程序使用了self来指定为调用该方法的THGWolf对象的 _name、 _age成员变量赋值,这样就可以把调用该方法时传入的参数赋值给 _name、 _age两个成员变量。

当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(int argc, char* argv[])
{
  @autoreleasepool {
    ReturnSelf* rt = [[ReturnSelf alloc] init];
    [[[rt grow] grow] grow];
    NSLog(@"rt的_age成员变量的值是:%d", rt-
         >_age);
  }
}

@public关键字用于暴露位于ta下面的所有成员变量。

可以看出,如果在某个方法中把self作为返回值,则可以多次连续调用同一个方法从而使代码更加简洁。但可能造成实际意义模糊,如上面的grow方法,用于表示对象的生长(_age成员变量+1),实际上不应该有返回值。

id类型

OC提供了一个id类型。这个id类型可以代表所有对象的类型,任意类的对象都可以赋给id类型的变量

当通过id类型变量来调用方法时,OC会执行动态绑定。所谓动态绑定,是指OC将会跟踪对象所属的类,它会在运行时判断该对象所属的类,并在运行时确定需要动态调用的方法,而不是在编译时确定要调用的方法。

以前面所创建的THGPerson类为例:

#import <Foundation/Foundation.h>
#import "THGPerson.h"

int main(int argc, char* argv[])
 {
  @autoreleasepool {
    id p = [[THGPerson alloc] init];  //将THGPerson对象赋给该id类型的变量
    [p say: @"你好,iOS!"];
  }
}

程序将会在运行时动态检测该变量所指的对象的实际类型THGPerson,因此,将会动态绑定到执行THGPerson对象的say:方法。

主打一个动态

方法详解

方法在逻辑上要么属于类,要么属于对象。

方法的所属性

实际上,方法是由传统的函数发展而来的。就行为来看,函数与方法具有极高的相似性,ta们都可以接受定义形参,调用时都可以传入实参。

方法与传统的函数具有显著不同:在结构化编程语言里,函数是一等公民,整个软件由一个个函数组成;在面向对象的编程语言里,类才是一等公民。因此,在OC语言里,方法不能独立存在,方法必须属于类或对象

OC语言中方法的所属性主要体现在如下几个方面。

  • 方法不能独立定义,只能在类体里定义。
  • 从逻辑上看,方法要么属于该类本身,要么属于该类的一个对象。
  • 不能独立调用方法,调用方法需要使用类或对象作为调用者。

参数个数可变的方法

前面多次使用到了NSLog()函数,这个函数可以传入任意多个参数,这就是形参个数可变的函数。如果在定义方法时,在最后一个参数名后增加逗号和三点(…),则表明该形参可以接受多个参数值。

下面定义一个形参个数可变的方法:

#import <Foundation/Foundation.h>

@interface VarAegs : NSObject
  - (void)test: (NSString*)name, ...;
@end

test:方法声明了一个NSString*类型的形参,这个形参除了指定name参数之外,还带, ...,这表明该方法还可接受个数可变的NSString*参数。

为获取个数可变的形参,则需要使用如下关键字:

  • va_list:这是一个类型,用于定义指向可变参数列表的指针变量。
  • va_start:这是一个函数,该函数指定开始处理可变形参的列表,并让指针变量指向可变形参列表的第一个参数。
  • va_end:结束处理可变形参,释放指针变量。
  • va_arg:该函数返回获取指针当前指向的参数的值,并将指针移动到指向下一个参数。
#import "VarArgs.h"

@implementation
  - (void)test: (NSString*)name, ...
{
  va_List argList;  //该指针变量指向可变参数列表
  if (name) {  //第一个name参数存在
    NSLog(@"%@", name);  //name参数并不在可变擦数列表中,因此先处理name参数
    va_start(argList, name);  //让argList指向第一个可变参数列表的第一个参数,开始提取可变参数列表的参数
    NSString* arg = va_arg(argList, id);  //va_arg用于提取argList指针当前指向的参数,并将指针移动到指向下一个参数;arg变量用于保存当前获取的参数,如果不为nil,则进入循环体
    while (arg) {
      NSLog(@"%@", arg);
      arg = va_arg(argList, id);
    }
    va_end(argList);//释放argList指针,结束提取
  }
}
@end
int main(int argc, char* argv[]) {
  @autoreleasepool {
    VarArgs* va = [[Varargs alloc] init];
    [va test: @"iOS", @"Android", @"Mac", nil];
  }
}

本质上,可变参数也是一个类似于数组的结构。

当程序调用test:方法时,为了明确告诉程序可变形参的结束点,可将最后一个参数设为nil,这样就可以保证while循环迭代获取可变形参时能正常跳出循环。

个数可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个个数可变的形参。

成员变量

在OC中,根据定义变量位置的不同,可将变量分成三大类:成员变量、局部变量、全局变量。无论是方法中定义的变量,还是函数中定义的变量,ta们都是局部变量。

成员变量及其运行机制

成员变量指的是在类接口部分或类实现部分定义的变量,OC的成员变量都是实例变量,OC并不支持真正的类变量。

实例变量可理解为实例成员变量,ta作为实例的一个成员,与实例共存亡。

只要实例存在,程序就可以访问该实例的实例变量,在程序中访问实例变量:

实例->实例变量

以下通过THGPerson实例来访问实例变量:

#import <Foundation/Foundation.h>

@interface THGPerson : NSObject
{
  @public
  NSString* _name;
  int _age;
}
@end
  
@implementation THGPerson
@end
  
int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGPerson* p = [[THGPerson alloc] init];
    NSLog(@"p变量的_name实例变量的值是:%@,p对象的_age成员变量的值是:%d", _name, _age);
    p->_name = @"Kevince";
    p->_age = 20;
    NSLog(@"p变量的_name实例变量的值是:%@,p对象的_age成员变量的值是:%d", _name, _age);
  }
}

从上面的程序可以看出,成员变量无须显式初始化,只要为一个类定义了实例变量,系统就会为实例变量执行默认初始化,基本类型的实例变量默认被初始化为0;指针类型的成员变量默认被初始化为nil

从内存存储的角度来看,OC的对象与C结构体相似。

模拟类变量

OC并不支持类似于Java的类变量,虽然类变量并不常用,但有时候还是需要使用它,下面可以通过内部全局变量来模拟类变量。

虽然OC也提供了static关键字,但这个static关键字不能用于修饰成员变量,ta只能修饰局部变量、全局变量和函数,static修饰局部变量表示将该局部变量存储到静态存储区;修饰全局变量用于限制该全局变量只能在当前源文件中访问;修饰函数用于限制该函数只能在当前源文件中被调用。

为了模拟类变量,可以在类实现部分定义一个static修饰的全局变量,并提供一个类方法来暴露该全局变量。以下示例为THGUser类提供了一个nation类变量。

THGUser的接口部分声明两个类方法分别用于修改或获取类变量,THGUser类的接口部分代码如下:

@interface THGUser : NSObject
  + (NSString*)nation;
  + (void)setNation: (NSString*)newNation;
@end

接下来在THGUser的实现部分定义一个static全局变量。

#import "THGUser.h"

@implementation THGUser
static NSString* nation = nil;

+ (NSString*)nation {
  return nation;  //返回nation全局变量
}

+ (void)setNation: (NSString*)newNation {
  if (![nation isEqualToString: newNation]) {
    nation = newNation;  //对nation全局变量赋值
  }
}
@end
  
int main(int argc, char* argv[]) {
  @autoreleasepool {
    [THGUser setNation: @"中国"];  //为THGUser的类变量赋值
    NSLog(@"THGUser的nation类变量为:%@", [THGUser nation]);  //访问THGUser的类变量
  }
}

定义一个内部全局变量,并实现两个类方法用于访问、修改该全局变量,这样就可以为某个类增加一个类变量。

单例(Singleton)模式

在某些时候,程序多次创建某个类的对象没有任何意义,还可能造成系统性能下降(因为频繁地创建对象、回收对象带来的系统开销问题),此时程序需要保证该类只有一个实例。例如,系统可能只有一个窗口管理器、一个假脱机打印设备或一个数据库引擎访问点,此时如果在系统中为这些类创建多个对象,就没有太大的实际意义了

如果一个类始终只能创建一个实例,则这个类就被称为单例类

单例类可通过static全局变量来实现,此处考虑定义一个static全局变量,该变量用于保存已创建的Singleton对象——每次程序需要获取该实例时,都需要先判断该static全局变量是否为nil,如果该全局变量为nil,则初始化一个实例并赋值给static全局变量;如果该全局变量不为nil,那么程序直接返回该全局变量指向的实例即可

THGSingleton类的接口部分:

#import <Foundation/Foundation.h>
@interface THGSingleton : NSobject
+ (id)instance;  //程序通过该类方法来获取该类的唯一实例
@end

在THGSingleton类的实现部分将会定义一个static全局变量,并通过该全局变量来缓存已有的实例,然后实现instance类方法,在该类方法中控制THGSingleton类最多只会产生一个实例。

THGSingleton类的实现部分:

#import "THGSingleton.h"

@implementation THGSingleton
static id instance = nil;
+ (id)instance {
  if (!instance) {  //instance全局变量为nil
    instance = [[super alloc] init];  //创建Singleton实例,并将该实例赋给instance全局变量
  }
  return instance;
}
@end
  
int main(int argc, char* argv[]) {
  @autoreleasepool {
    NSLog(@"%d", [THGSingleton instance] == [THGSingleton instance]);  //判断两次获取的实例是否相等,程序将会返回1
  }
}

隐藏和封装

面向对象语句都推荐对类进行良好的封装。

理解封装

**封装(Encapsulation)**是面向对象的三大特征之一,ta指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问

封装是面向对象编程语言对客观世界的模拟,在客观世界中对象的成员变量(用于描述对象的状态数据)都被隐藏在对象内部,外界无法直接操作和修改。

如通常不能随意修改THGPerson对象的_age成员变量。对一个类或对象实现良好的封装,可以实现以下目的

  • 隐藏类的细节。
  • 让使用者只能通过实现预定的_方法_来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问。
  • 可进行数据检查,从而有利于保证对象信息的完整性。
  • 便于修改,提高代码的可维护性。

为了实现良好的封装,需要从以下两个方面考虑:

  • 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
  • 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。

因此,封装实际上有两方面的含义:==把该隐藏的隐藏起来,把该暴露的暴露出来。==这两个方面都需要通过使用OC提供的访问控制符来实现。

使用访问控制符

OC提供了4个访问控制符:@private@package@protected@public,分别代表4个访问控制级别。OC的访问控制级别由小到大如图所示。

在这里插入图片描述

  • @private(当前类访问权限):如果类的成员变量使用@private访问控制符来限制,则这个成员变量只能在当前类的内部访问。很显然,这个访问控制符用于彻底隐藏成员变量。在类的实现部分定义的成员变量相当于默认使用这种访问权限
  • @package(相同映像访问权限):如果类的成员变量使用@package访问控制符来限制,则这个成员变量可以在当前类以及当前类的同一个映像的任意地方访问。很显然,这个访问控制符用于部分隐藏成员变量
  • @protected(子类访问权限):如果类的成员变量使用@protected访问控制符来限制,则这个成员变量可以在当前类、当前类的子类的任意地方访问。很显然,这个访问控制符用于部分暴露成员变量在类的接口部分定义的成员变量默认使用这种访问权限
  • @public(公共访问权限):这是一个最宽松的访问控制级别,如果类的成员变量使用@public限制,那么这个成员变量可以在任意地方访问,不管是否处于同一映像中,也不管是否具有父子继承关系。

最后,使用表格来总结上述访问控制级别。

000000000000000

通过上面关于访问控制符的介绍不难发现,访问控制符用于控制类的成员变量是否可以被其他类访问。

对于局部变量而言,其作用域就是ta所在的方法,不可能被其他类访问,因此,不能使用访问控制符来修饰

下面通过使用合理的访问控制来定义一个THGPerson类,这个THGPerson类就实现了良好的封装:

#import <Foundation/Foundation.h>

@interface THGPerson : NSObject {
  @private  //表明以下成员变量都只能在当前类中访问
  NSString* _name;
  int _age;
}

- (void)setName: (NSString*)name;
- (NSString*)name;
- (void)setAge: (int)age;
- (int) age;
@end

@private@package@protected@public这4个访问控制符相当于开关,从ta们出现的位置开始,到下一个访问控制符或右花括号之间的成员变量,都受该访问控制符的控制。

接下来程序为THGPerson类的两个成员变量分别提供了settergetter方法来设置成员变量,获取成员变量的值。

THGPerson类的实现部分如下:

#import "THGPerson.h"

@implementation THGPerson
  - (void)setName: (NSString*)name {  //设置_name成员变量
    if ([name length] > 7 || [name length] < 2) {
      NSLog(@"您设置的人名不符合要求");
      return;
    } else {
      _name = name;
    }
  }
  - (NSString*)name {  //获取_name成员变量的值
    return _name;
  }
  
  - (void)setAge: (int)age {  //设置_age成员变量
    if (_age != age) {
      if (age > 100 || age < 0) {
        NSLog(@"您设置的年龄不合法");
        return;
      } else {
        _age = age;
      }
    }
  }
  - (int)age {
    return _age;  //获取_age成员变量的值
  }
@end

定义上面的THGPerson类之后,该类的两个成员变量只能在当前类的内部才能访问,在THGPerson类之外只能通过各自对应的settergetter方法来操作和访问

OC类中成员变量的settergetter方法有非常重要的意义。如果一个OC类的每个成员变量都使用**@private限制**,并为每个成员变量都提供了settergetter方法,那么这个类就是一个符合规范的类

下面在主函数中尝试操作和访问该对象的成员变量:

#import "THGPerson.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGPerson* p = [[THGPerson alloc] init];
    p->_age = 1000;  //_age成员变量已被隐藏,因此会报错
    [p setAge: 1000];  //此处设置不合法,未成功设置_age,故此处输出0
    NSLog(@"未能设置_age成员变量:%d", [p age]);
    [p setAge: 20];
    NSLog(@"成功设置成员变量后:%d", [p age]);
    
    [p setName: @"Kevince"]NSLog(@"成功设置_name成员变量后:%@", [p age]);
  }
}

当使用setter方法来设置_name_age两个成员变量时,就允许程序员在setter方法中增加自己的控制逻辑,从而保证THGPerson对象的两个成员变量不会出现与实际不符的情形

一个类常常就是一个小的模块,应该只让这个模块公开必须让外界知道的内容,而隐藏其他内容。设计程序时,应尽量避免一个模块直接操作和访问另一个模块的数据,模块设计追求高内聚(尽可能把模块的内部数据、功能实现细节隐藏在模块内部独立完成,不允许外部直接干预)、低耦合仅暴露少量的方法给外部使用)。正如常见的内存条,内存条里的数据及其实现细节被完全隐藏在内存条中,外部设备(如主机板)只能通过内存条的金手指**(提供一些方法供外部使用)**和内存条进行交互。

关于访问控制符的使用,存在如下几条基本原则:

  • **类里的绝大部分成员变量都应该使用@private限制。**除此之外,有些方法还只是用于辅助实现该类的其他方法,这些方法被称为工具方法,这些工具方法也应该被隐藏在该类的内部,此时就应该把这些方法定义在类实现部分
  • 如果某个类主要作为其他类的父类,该类里包含的成员变量希望被子类访问,则可考虑使用@protected限制这些成员变量。
  • 希望暴露出来给其他类自由调用的方法应该先在类接口部分定义,然后在类实现部分实现它们

@private访问控制符的作用非常明确,ta将收到该访问控制符限制的成员变量限制在当前类内部**(与在类实现部分定义的成员变量的作用域类似)**;@public访问控制符则用于彻底暴露受ta控制的成员变量;@protected则让那些受ta控制的成员变量不仅可以在当前类中访问,也可以在其子类中访问。

但前文关于@package访问控制符的描述则有些不太好理解,下面来详细学习一下此访问控制符。

理解@package访问控制符

@package让那些受ta控制的成员变量不仅可以在当前类中访问,也可以在同一映像中访问——关键是何为**“同一映像”**。

所谓“同一映像”。就是编译后生成的同一个框架或同一个执行文件。比如,想开发一个基础框架,如果使用@private限制某个成员变量,则限制得太死——考虑该框架中其他类、其他函数可能也需要直接访问该成员变量,但该框架又不希望其他外部程序访问该成员变量,此时就可以考虑使用@package来限制该成员变量。

当编译器最后把@private限制的成员变量所在的类、其他类和函数编译成一个框架库之后,这些类、函数都在同一个映像中,此时这些类、函数都可以自由访问这个@private限制的成员变量。但其他程序引用这个框架库时,由于其他程序只是依赖这个框架库,其他程序与该框架库就不在同一个映像中,因此,其他程序无法访问这个@private限制的成员变量。

下面定义一个THGApple类作为示例:

#import <Foundation/Foundation.h>

@interface THGApple : NSObject {
  @package
  double _weight;
}
@end

_weight成员变量被@package限制,因此ta可以被“同一映像”中的其他类、函数自由访问。

看看主函数中直接访问THGApple对象的_weight成员变量:

#import "THGApple.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGApple* apple = [[THGApple alloc] init];
    apple->_weight = 30.7;
    NSLog(@"apple的重量为:%g", apple->_weight);
  }
}

使用如下命令来编译上面的程序:

clang -fobjc-arc -framework Foundation THGApple.m THGAppleTest.m

上面的命令将会将会编译THGApple.h、THGApple.m、THGAppleTest.m文件并生成一个a.out执行文件,由于THGApple类和main()函数位于同一个映像中,因此main()函数可以自由访问THGApple类的_weight成员变量。

合成存取方法

前面介绍了为成员变量自己实现setter方法和getter方法,这种做法虽然不难,但如果一个类中包含很多个成员变量时,为每个成员变量都编写settergetter方法都将显得乏味无趣。

如今版本的OC,ta自动合成了setter方法和getter方法,这样开发人员就可以避免书写乏味的settergetter方法了。而且如果用户需要自己控制某个方法的实现,开发者依然可以提供settergetter方法,这是用户提供的settergetter方法将会覆盖系统自动合成的settergetter方法。

让系统自动合成settergetter方法需要如下两步:

  1. 在类接口部分使用@property指令定义属性。使用@peoperty定义属性时无须放在类接口部分的花括号里,而是直接放在@interface@end之间定义。@property指令放在属性定义的最前面
  2. 此步是可选的。如果程序需要改变gettersetter方法对应的成员变量的变量名,则可以在**类实现部分使用@synthesize**指令。

当采用上述两个步骤合成存取方法之后,不仅会合成成对的settergetter方法,还会自动在类实现部分增加一个成员变量,该成员变量的变量名为getter方法加下划线前缀

如果为某个类定义了一个成员变量,并提供了相应的存取方法,那么可称为定义了一个属性(property)

Xcode编码规范推荐将成员变量名定义为以下划线开头,在通过Xcode生成的模版代码中可以看到如下代码:

@synthesize window = _window;

上面的代码用于告诉系统合成的property对应的成员变量为_window,而不是window

@synthesize使用语法如下:

@synthesize 属性名称 = 成员变量名;

如果@synthesize指令后没有指定成员变量名,那么成员变量名默认与合成的getter方法同名。如果希望@property合成的属性对应的成员变量名保持为getter方法名加下划线前缀,则可省略@synthesize定义。

以下为合成存取方法的用法示例:

#import <Foundation/Foundation.h>

@interface THGUser : NSObject
@property (nonatomic)NSString* name;  //nonatomic指示符有额外的作用,后续进行详细介绍
@property NSString* pass;
@property NSDate* birth;
@end

以上代码使用@property定义了3个属性,这表明程序将会合成3组gettersetter方法。考虑要对name属性的setter方法进行额外控制,因此还打算在类实现部分实现setName:方法。

类实现部分:

#import "THGUser.h"

@implementation THGUser
//指定name属性底层的成员变量名为_name  
@synthesize name = _name;  //定义的成员变量名本来就是_name,因此这行代码可以省略
@synthesize pass;
@synthesize birth;

- (void)setName: (NSString*)name {  //实现自定义的setName:方法,添加自己的控制逻辑
  self->_name = [NSString stringWithFormat: @"+++%@", name];  //当程序调用setName:方法进行设置时,系统对_name成员变量所赋的值会添加+++前缀
}

下面来测试该THGUser类:

#import "THGUser.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGUser* user = [[THGUser alloc] init];
    
    [user setName: @"admin"];
    [user setPass: @"1234"];
    [user setBirth: [NSDate date];
    //对user成员变量的值进行访问 
    NSLog(@"管理员账号为:%@,密码为:%@,生日为:%@", [user name], [user pass], [user birth]);
  }
}

正如接口部分所看到的那样,当@property定义属性时,还可以在@property和类型之间用括号添加一些额外的指示符,可使用的特殊指示符如下:

  • assign:该指示符指定对属性仅进行简单赋值,不更改所赋的值的引用计数。这个指示符主要适用于NSInteger基本类型,以及short、float、double、结构体等各种C数据类型。

    引用计数是OC内存回收的概念,当一个对象的引用计数大于0时,表明该对象还不应该被回收。由于NSInteger等基础类型,以及short、float、double、结构体等各种C数据类型都不存在内存回收问题,因此使用assign即可。

  • atomic(nonatomic):指定合成的存取方法是否为原子操作。所谓原子操作,主要指是否线程安全。如果使用atomic,那么合成的存取方法都是线程安全的——当一个线程进入存取方法的方法体之后,其他线程无法进入该存取方法,这样就可以避免多线程并发破坏对象的数据完整性,atomic时默认值。虽然atomic可以保证对象数据的完整性,但atomic的线程安全会造成性能下降,因此,在大多数单线程环境下,都会考虑使用nonatomic来提高存取方法的访问性能。

  • copy:如果使用copy指示符,那么调用setter方法对成员变量赋值时,会将被赋值的对象复制一个副本,再将该副本(对象)赋值给成员变量。copy指示符会将原成员变量所引用对象的引用计数减1。当成员变量的类型是可变类型,或其子类时可变类型时,被复制的对象有可能在赋值之后被修改,如果程序不需要这种修改影响setter方法设置的成员变量的值,此时就可考虑用copy指示符。

下面定义一个THGBook类来示范copy指示符的作用。该类中定义了一个NSString类型的属性,NSString类有一个可变子类:NSMutableString。

THGBook类接口部分:

#import <Foundation/Foundation.h>

@interface THGBook : NSObject
@property (nonatomic)NSString* name;
@end

需要指出的是,name的类型是NSString,而NSString有一个NSMutableString子类,当把NSMutableString对象赋值给THGBook对象的name属性之后,NSMutableString对象可能被修改——由于定义name时未使用copy指示符,因此,NSMutableString对象的修改将会影响THGBook对象的name属性值。

THGBook类的测试代码:

#import "THGBook.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGBook* book = [[THGBook alloc] init];
    NSMutableString* str = [NSMutableString stringWithString: @"iOS"];
    [book setName: str];  //对book的成员变量赋值
    NSLog(@"book的name为:%@", [book name]);
    
    [str appendString: @"是Apple公司发行的操作系统"];  //修改str字符串
    NSLog(@"book的name为:%@", [book name]);  //将会看到book的那么属性也会被修改
  }
}

一共两次输出book的name属性值,但由于book的name属性值和str指向同一个NSMutableString对象,因此当程序修改str指向的NSMutableString对象时,book的name属性值也会随之改变。

对上面程序进行修改:

@property (nonatomic, copy)NSString* name;

这样当程序执行[book setName: str]时,程序会将str指向的NSMutableString对象复制一个副本。再将副本作为setName:的参数值,当程序通过str修改底层NSMutableString对象时,THGBook对象的name属性值不会随着改变

  • getter、setter:这两个指示符**用于为合成的getter、setter方法指定自定义方法名。**示例如下:

    #import <Foundation/Foundation.h>
    
    @interface THGItem : NSObject
    //指定自定义的getter、setter方法名
    @property (assign, nonatomic, getter = wawa, setter = nana:)int price;  //注意,setter方法要带参数,所以要带冒号
    @end
    

    设置并访问THGItem对象的price属性:

    #import "THGItem.h"
    
    int main(int argc, char* argv[]) {
      @autoreleasepool {
        THGItem* item = [[THGItem alloc] init];
        [item nana: 30];
        NSLog(@"item的price为:%d", [item wawa]);
      }
    }
    
  • readonly、readwrite:readonly指示系统只合成getter方法,不再合成setter方法;readwrite是默认值,指示系统需要合成setter、getter方法。

  • retain:使用retain指示符定义属性时,当把某个对象赋值给该属性时,该属性原来所引用的对象的引用计数减1,被赋值对象的引用计数加1。

    在未启用ARC机制的情况下,retain是一个很有用的指示符:当一个对象的引用计数大于1时,该对象不应该被回收,但启用ARC机制之后,一般就较少使用retain指示符了。

    下面定义了一个THGWin类作为示例:

    #import <Foundation/Foundation.h>
    
    @interface THGWin : NSObject
    @property (nonatomic, retain)NSDate* date;
    @end
    

    测试代码:

    #import "THGWin.h"
    
    int main(int argc, char* argv[]) {
      THGWin* win = [[THGWin alloc] init];
      NSDate* date = [NSdate date];
      
      NSLog(@"date的引用计数为:%ld", date.retainCount);  //1
      
      [win setDate: date];
      NSLog(@"date的引用计数为:%ld", date.retainCount);  //2
      
      [date release];  //释放date的引用计数
      NSLog(@"[win date]的引用计数为:%ld", [win date].retainCount);  //1
    }
    

    上面的程序必须关闭ARC机制,因此没有使用@autoreleasepool,而且使用clang命令编译生成时,也不应该使用-fobjc-arc选项(用于启用ARC机制)。也就是说,使用clang -framework Foundation THGWin.m THGWinTest.m命令即可。

  • strong、weak:strong指示符指定该属性被赋值对象持有强引用,而weak指示符指定该属性对被赋值对象持有弱引用。强引用的意思是,只要该强引用指向被赋值的对象,那么该对象就不会自动回收;弱引用的意思是,即使该弱引用指向被赋值的对象,该对象也可能被回收

  • unsafe_unretained:这个指示符与weak指示符基本相似,对于只被unsafe_unretained指针所指的对象,该对象也可能被回收。与weak指针不同的是,当unsafe_unretained指针所引用的对象被回收后,unsafe_unretained指针不会被赋值为nil,因此这可能导致程序崩溃。一般来说,使用unsafe_unretained指示符不如使用weak指示符。

    在启动ARC机制是,使用strong、weak指示符将十分方便。如果程序不希望被该属性引用的对象被回收,那就应该使用strong指示符;如果程序需要保证性能,避免内存溢出,则可以使用weak指示符。使用weak指示符时需要小心,当程序通过该weak属性来访问被引用的对象时,该对象可能已经被回收了。对于声明为weak的指针,指针指向的地址一旦被释放,这些指针都将被赋值为nil,这样能有效地防止悬空指针weak指示符可有效地防止悬空指针

使用点语法访问属性

OC允许使用简化的点语法_访问_属性和对属性_赋值_。

点语法只是一种简化写法,其本质依然是调用对象(无论该对象是否存在对应的成员变量)的getter方法来获取属性值,和调用setter方法来设置对象的属性值。

下面定义了一个THGCard类,该类代表扑克牌,接口部分如下:

#import <Foundation/Foundation.h>

@interface THGCard : NSObject
@property (nonatomic, copy)NSString* flower;  //花色
@property (nonatomic, copy)NSString* value;  //牌面值
@end

测试代码:

#import "THGCard.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGCard* card = [[THGCard alloc] init];
    card.flower = @"♠️";
    card.value = @"A";
    NSLog(@"我的扑克牌为:%@%@", card.flower, card.value);
  }
}

对象初始化

创建对象的语法有两种,常用的是[[类名 alloc] init],另外还有一种不太常用的[类名 new]每次创建对象时,总是需要使用alloc方法为对象分配内存空间

为对象分配空间

无论创建哪个对象,最先总需要调用该类的alloc类方法来分配内存,实际上,这个alloc类方法来自NSObject,而所有的OC类都是NSObject的子类,因此,所有的类都可以调用alloc类方法来分配内存

当程序调用某个类的alloc类方法时,系统完成如下事情:

  1. 系统为该对象的所有实例变量分配内存空间。
  2. 将每个示例变量的内存空间都重置为0。具体地说,所有的整形变量所在空间都重置为0;所有的浮点型变量所在空间都重置为0.0;所有的BOOL变量都重置为NO;所有的指针类型变量都重置为nil。

仅仅分配内存空间的对象还不能使用,必须先对该对象执行初始化,然后才可以调用ta。OC最常用的初始化方法为init,该方法同样来自NSObject。

[类名 alloc]此代码创建的对象也能运行,但由于没有对对象执行初始化,挺次调用该对象的方法时可能出现未知的结果。

初始化方法与对象初始化

NSObject提供的init方法虽然可以完成初始化,但由于ta只是完成最基本的初始化,因此,对象的所有成员变量依然为0

在实际编程过程中,可以提供自己的init方法,其实就是重写NSObject的init方法。当重写init方法时,开发者可以加入任意的自定义处理代码对属性执行初始化

下面定义了THGUser类的接口部分:

#import <Foundation/Foundation.h>

@interface THGUser : NSObject
@property (nonatomic, copy)NSString* name;
@property (nonatomic, assign)int age;
@property (nonatomic, copy)NSString* address;
@end

接下来为该THGUser类提供实现部分,在实现部分重写NSObject的init方法,用于完成自定义初始化:

#import "THGUser.h"

@implementation THGUser
- (id)init {
if (self = [super init]) {  //调用父类init方法执行初始化,将初始化的到的对象赋给self对象,表达式不为nil,则表明父类的init方法初始化成功
  self->_name = @"Kevince";
  self->_age = 20;
  self->_address = @"Xi'an";
}
return self;
}
@end

先调用符类的init方法执行默认初始化,最后返回一个已经初始化完成的THGUser对象。

测试代码:

#import "THGUser.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGUser* user = [[THGUser alloc] init];
    
    NSLog(@"user的name、age、address分别为%@、%d、%@", user.name, user.age, user.address);
  }
}

以上代码将会为THGUser对象分配内存空间,并调用init方法执行初始化,只不过这个init方法不再是NSObject提供的,而是上面的TGGUser.m程序提供的。这样就可以保证新生成的THGUser对象的属性已经被赋值。

便利的初始化方法

前面只是重写了init方法,这个方法对THGU色入执行的初始化总是固定的,不能根据参数执行动态初始化。实际上还会根据业务需要,为OC类提供更多便利的初始化方法,这些初始化方法通常会以init开头,并允许自带一些参数

下面定义了THGCar类,该类会自定义一些更便利的初始化方法,这些初始化方法可以更加灵活地初始化THGCar对象的属性。

THGCar类的接口部分;

#import <Foundation/Foundation.h>

@interface THGCar : NSObject
@property (nonatomic, copy)NSString* brand;
@property (nonatomic, copy)NSString* model;
@property (nonatomic, copy)NSString* color;

- (id)initWithBrand: (Nsstring*)brand model: (NSString*) model;
- (id)initWithBrand: (NSString*)brand model: (NSString8) model: color: (NSString*)color;
@end

实现部分:

#import "THGCar.h"

@implementation THGCar
- (id)init {
  if (self = [super init]) {
    self->_brand = @"奥迪";
    self->_model = @"Q5";
    self->_color = @"黑色";
  }
  return self;
}

- (id)initWithBrand: (NSString*) brand model: (NSString*)model {
  if (self = [super init]) {
    self->_brand = brand;
    self->_model = model;
    self->_color = @"黑色";
  }
  return self;
}

- (id)initWithBrand: (NSString*)brand model: (NSString*)model color: (NSString*)color {
  if (self = [self initWithBrand: brand model: model]) {
    self->_color = color;
  }
  return self;
}
@end

上面程序中,先调用父类的init方法执行默认初始化,第3个初始化方法直接复用了第2个初始化方法,因此可以提供更好的代码复用。

如果系统中包含了多个初始化方法,其中一个初始化方法执行体中完全包含另一个初始化方法的执行体,如图所示。

在这里插入图片描述

从图中可以看出,初始化方法B中完全包含了初始化方法A。对于这种完全包含的情况,就可以直接在初始化方法B中调用初始化方法A,这样就可以更好地进行代码复用

下面时THGCar的测试代码:

#import "THGCar.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGCar* car1 = [[THGCar alloc] init];
    NSLog(@"car1的brand为%@", car1.brand);
    NSLog(@"car1的model为%@", car1.model);
    NSLog(@"car1的color为%@", car1.color);
    
    THGCar* car2 = [[THGCar alloc] initWithBrand: @"奔驰" model: @"ML350"];
    NSLog(@"car2的brand为%@", car2.brand);
    NSLog(@"car2的model为%@", car2.model);
    NSLog(@"car2的color为%@", car2.color);
    
    THGCar* car3 = [[THGCar alloc] initWithBrand: @"宝马" model:@"X5" color: @"BLACK"];
    NSLog(@"car3的brand为%@", car3.brand);
    NSLog(@"car3的model为%@", car3.model);
    NSLog(@"car3的color为%@", car3.color);
  }
}

通过这些便利的初始化方法,程序可以在创建对象时立即初始化对象的属性,避免对象创建完成后还要通过调用对象的setter方法或点语法来设置对象的属性值

类的继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。OC的继承具有单继承的特点,每个子类只有一个直接父类

继承的特点

实现继承的类被称为子类,被继承的类被称为父类,也称其为基类、超类。父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。

OC里子类继承父类的语法如下:

@interface SubClass : SuperClass {
  //成员变量定义
}
//方法定义部分
@end

继承关系的本质是一种“由一般到特殊”的关系,子类继承父类,子类是一种特殊的父类。从这个意义上看,使用扩展来描述子类和父类的关系可能更合适。也就是说:Apple类扩展了Fruit类。

当子类扩展父类时,子类可以继承得到父类的如下东西:

  • 全部成员变量
  • 全部方法(包括初始化方法)

下面定义一个THGFruit类来示范子类继承父类的特点:

#import <Foundation/Foundation.h>

@interface THGFruit : NSObject
@property (nonatomic, assign)double weight;
- (void)info;
@end

实现部分:

#import "THGFruit.h"

@implementation THGFruit
- (void)info {
  NSLog(@"我是一个水果!重%gg", self.weight);
}
@end

接下来再定义该THGFruit类的子类THGApple。类接口部分如下:

#import <Foundation/Foundation.h>
#import "THGFruit.h"

@interface THGApple : THGFruit
@end

来测试一下这个THGApple类:

#import "THGApple.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGApple* a = [[THGApple alloc] init];
    a.weight = 57;
    [a info];
  }
}

上面程序表明THGApple对象也具有weight属性和info方法,这就是继承的作用。

OC语言摒弃了C++中难以理解的多继承特征,即每个类最多只有一个父类。例如,下面的代码将会引起编译错误:

@interface SubClass : Base1, Base2, Base3{...} @end

OC类只能有一个直接父类,可以有无限多个间接父类。例如:

@interface THGFruit : THGPlant {...} @end
@interface THGApple : THGFruit {...} @end

实际上,定义任何一个OC类都需要指定一个直接父类,在默认情况下,定义的OC类需要继承NSObject类,因此,NSObject是所有类的父类,要么是其直接父类,要么是其间接父类。

从子类角度来看,子类扩展(extend)了父类;但从父类的角度来看,父类派生(derive)出了子类。也就是说,扩展和派生所描述的是同一个动作,只是观察角度不同而已

重写父类的方法

子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的成员变量和方法。但有一种情况例外:子类需要重写父类的方法

例如,鸟类都包含了飞翔的方法,其中鸵鸟是一种特殊的鸟类,因此,鸵鸟应该是鸟的子类,ta也将从鸟类获得飞翔的方法,但这个飞翔的方法明显不适合鸵鸟,为此鸵鸟需要重写鸟类的方法。

下面定义一个THGBird类,接口部分如下:

#import <Foundation/Foundation.h>

@interface THGBird : NSObiect
- (void)fly;
@end

实现部分:

#import <Foundation/Foundation.h>
#import "THGBird.h"

@implementation THGBird
- (void)fly {
  NSLog(@"我在天空自由自在地飞翔...");
}
@end

下面再定义一个THGOstrich类,这个类扩展了THGBird类,重写了THGBird类的fly方法。THGOstrich类的接口部分代码如下:

#import <Foundation/Foundation.h>
#import "THGBird.h"

@interface THGOstrich : THGBird
@end

当子类要重写父类方法时,子类接口部分并不需要重新声明要重写的方法,只要在类实现部分直接重写该方法即可。THGOsrtich类的实现代码如下:

#import <Foundation/Foundation.h>
#import "THGOstrich.h"

@implementation THGOstrich
- (void)fly {
  NSLog(@"我只能在地上奔跑...");
}
@end

测试代码:

#import "THGOstrich.h"

int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGOstrich* os = [[THGOstrich alloc] init];
    [os fly];
  }
}

这种子类包含与父类同名方法的现象被称为方法重写,也被称为方法覆盖(Override)。可以说,子类重写了父类的方法,也可以说子类覆盖了父类的方法。

方法重写必须注意方法签名关键字要完全相同,也就是方法名和方法签名中的形参标签都需要完全相同,否则就不能算方法重写

super关键字

如果在需要在子类方法中调用父类被覆盖的方法,则可使用super关键字来调用父类被覆盖的方法。为上面的THGOstrich类添加一个方法,在这个方法中调用THGBird被覆盖的fly方法。

- (void)callOverrideMethod {
  [super fly];  //在子类方法中通过super显式调用父类被覆盖的实例方法 
}

通过callOverrideMethod方法的帮助,就可以让THGOstrich对象既可以调用自己重写的fly方法,也可以调用THGBird类中被覆盖的fly方法。

  1. super是OC提供的一个关键字,用于限定该对象调用ta从父类继承得到的属性或方法

super既可以出现在类方法中,也可以出现在实例方法中。在类方法中使用super调用父类的方法时,被调用的父类方法只能是类方法;在实例方法中使用super调用父类的方法时,被调用的父类方法只能是实例方法。

  1. 当子类继承父类时,子类可以获得父类中定义的成员变量,因此,子类接口部分不允许定义与父类接口部分重名的成员变量(即使@private将成员变量限制在当前类的内部)。

实际上,无论父类接口部分的成员变量使用何种访问控制符限制,子类接口部分定义的成员变量都不允许父类接口部分定义的成员变量重名

需要指出的是,在类实现部分定义的成员变量将被限制在该类的内部,因此,父类在类实现部分定义的成员变量对子类没有任何影响。无论是接口部分还是实现部分,子类定义的成员变量都可以与父类实现部分定义的成员变量同名。

反过来也一样,在子类实现部分定义的成员变量也不受父类接口部分定义的成员变量的影响。

  1. 当子类实现部分定义了与父类重名的成员变量时,子类的成员变量就会隐藏父类的成员变量。因此,子类方法很难直接访问到成员变量,此时,可调用父类的方法来访问父类中被隐藏的成员变量

看以下示例。父类的接口部分如下:

#import <Foundation/Foundation.h>

@interface THGParent : NSObject {
  int _a;
}
@property (nonatomic, assign)int a;
@end

父类实现部分:

#import <Foundation/Foundation.h>
#import "THGParent.h"

@implementation THGParent
- (id)init {
  if (self = [super init]) {
    self->_a = 5;
  }
  return self;
}
@end

上面程序使用了@property来定义名为a的属性,这个名为a的属性实际用于操作_a成员变量。

接下来为THGParent定义一个子类,接口部分如下:

#import <Foundation/Founadtion.h>
#import "THGParent.h"

@interface THGSub : THGParent
- (void)accessOwner;
@end

实现部分,会定义一个名为_a的成员变量,该成员变量将会隐藏父类的成员变量:

#import <Foundation/Foundation.h>
#import "THGSub.h"

@implementation THGSub {
  int _a;  //该成员变量将会隐藏父类的成员变量
}

- (id)init {
  if (self = [super init]) {
    self->_a = 7;
  }
  return self;
}

- (void)accessOwner {
  NSLog(@"子类中_a的成员变量:%d", _a);
  NSLog(@"父类中被隐藏的_a成员变量:%d", super.a);
}
@end
  
int main(int argc, char* argv[]) {
  @autoreleasepool {
    THGSub* sub = [[THGSub alloc] init];
    [sub accessOwner];
  }
}

程序通过super关键字强制指定调用父类的a属性(实际上就是获取getter方法返回值),通过这种方式可以访问到父类中被隐藏的成员变量。

虽然程序只是创建了一个THGSub对象,但该对象内部依然有两块内存来保存_a成员变量,其中一块内存保存父类中被隐藏的 _a成员变量,可以通过父类中定义的方法来访问;另一块内存保存子类实现部分定义的 _a成员变量,可以在子类方法中直接访问

多态

OC指针类型的变量有两个:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)

多态性

先定义一个THGBase类,该THGBase类的接口部分如下:

#import <Foundation/Foundation.h>

@interface THGBase : NSObject
- (void)base;
- (void)test;
@end

THGBase类实现部分:

#import <Foundation/Foundation.h>
#import "THGBase.h"

@implementation THGBase
- (void)base {
  NSLog(@"父类的普通base方法");
}
- (void)test {
  NSLog(@"父类的将被覆盖的test方法");
}
@end

接下来为THGBase类定义一个子类,该子类将会覆盖符类中定义的test方法。下面时THGSubclass子类的接口部分:

#import <Foundation/Foundation.h>
#import "THGBase.h"

@interface THGSubClass : THGBase
- (void)sub;
@end

子类新增了一个sub方法。接下来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值