类的继承
继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Objective-C的继承具有单继承的特点,每个子类只有一个直接父类。
(1)继承的特点
Objective-C的继承通过“:父类”语法来实现,实现继承的子类被称为子类,被继承的类被称为父类,也称其为基类、超类。父类和子类的关系是一种“一般和特殊”的关系。例如,水果和苹果的关系,苹果继承了水果,苹果是水果的子类,苹果是一种特殊的水果。
因为子类是一种特殊的父类,因此,父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。
Objective-C里子类继承父类的语法格式如下:
@interface SubClass: SuperClass {
//成员变量定义
}
//方法定义部分
@end
从上面的语法格式来看,我们在原来的类定义上增加了“:SuperClass”即表明该子类继承了SuperClass类
当子类扩展父类时,子类可以继承得到父类的如下东西:
(1)全部成员变量。
(2)全部方法(包括初始化方法)。
用下面的Fruit类的代码示范子类继承父类的特点:
#import <Foundation/Foundation.h>
@interface Fruit: NSObject {
double _weight;
}
- (void) setWeight: (double)weight;
- (void) info;
@end
下面是Fruit类的实现部分,实现部分负责实现info方法,程序如下:
#import "Fruit.h"
@implementation Fruit
- (void) setWeight: (double)weight {
_weight = weight;
}
- (void) info {
NSLog(@"我是一个重%gg的水果!", self.weight);
}
@end
接下来再定义该Fruit类的子类Apple,类接口部分如下:
#import <Foundation/Foundation.h>
#import "Fruit.h"
@interface Apple: Fruit
@end
子类Apple的实现部分如下:
//由于Apple类本来只是一个没有定义任何属性和方法的空类,所以实现部分十分简单
#import "Apple.h"
@implementation Apple
@end
下面写一个主函数程序来测试一下这个Apple类:
#import "Apple.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
//创建Apple对象
Apple* a = [[Apple alloc] init];
//Apple对象本身没有自己的weight属性
//因为Apple的父类有weight属性,所以也可以访问Apple对象的weight属性
[a setWeight:56];
//调用Apple对象的info方法
[a info];
}
return 0;
}
上面程序的main()函数创建了Apple对象之后,可以访问Apple对象的weight属性和info方法,这表明Apple对象也具有weight属性和info方法,这就是继承的作用
由于Objective-C语言的继承具有单继承的特点,即每个子类只有一个直接父类。
所以下面的代码会引起编译错误:
//由于下面的代码会引起编译错误:
@interface SubClass: Base1, Base2, Base3 {
//成员变量
}
@end
另外,虽然Objective-C类只能有一个直接父类,但是却可以有无限个间接父类,例如:
//Plant是Fruit的直接父类
@interface Fruit: Plant {
//成员变量
}
@end
//Fruit是Apple的直接父类
@interface Apple: Fruit {
//成员变量
}
@end
上面的类定义中,Fruit是Apple的父类,Plant也是Apple的父类,区别是Fruit是Apple的直接父类,而Plant是Apple的间接父类
实际上,在定义任何Objective-C类时都需要指定一个直接父类,在默认情况下自定义的Objective-C类都需要继承NSObject类,因此,NSObject是所有类的父类,要么是其直接父类,要么是其间接父类,因此,所有的Objective-C对象都可以调用NSObject类所定义的实例方法。
从子类角度来看,子类扩展了父类;但从父类角度来看,父类派生出了子类。
(2)重写父类的方法
子类扩展了父类,子类是一个特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的成员变量和方法。但有些时候,子类需要重写父类的方法。例如,鸟类都包含了飞翔的方法,但其中鸵鸟是一种特殊的鸟类,因此,鸵鸟因该是鸟的子类,它也将从鸟类获得飞翔的方法,但是这个飞翔的方法明显不适合鸵鸟,因此,鸵鸟需要重写鸟类的方法。
下面的程序先定义了一个Bird类,该类的接口部分代码如下:
#import <Foundation/Foudation.h>
@interface Bird: NSObject
- (void) fly;
@end
下面是Bird类的实现部分:
#import <Foundation/Foundation.h>
#import "Bird.h"
@implementation Brid
- (void) fly {
NSLog(@"我可以在天空飞翔");
}
@end
下面再定义一个Ostrich类,这个类扩展了Bird类,重写了Bird类的方法,Ostrich类的接口部分代码如下:
#import <Foundation/Foundation.h>
#import "Bird.h"
@interface Ostrich: Bird
@end
从上面我们可以看到,当子类要重写父类方法时,子类接口部分并不需要重新声名所要重写的方法,只要在类实现部分直接重写该方法即可。
下面是Ostrich类的实现部分代码:
#import <Foundation/Foundation.h>
#import "Ostrich.h"
@implementation Ostrich
//重写父类的fly方法
- (void) fly {
NSLog(@"我只能在地上跑");
}
@end
接下来写主函数来测试Ostrich类:
#import <Foundation/Foundation.h>
#import <Ostrich.h>
int main(int argc, cahr* argv[]) {
@autoreleasepool {
//创建Ostrich对象
Ostrich* b = [[Ostrich alloc] init];
//执行Ostrich对象的fly方法,执行后输出“我只能在地上跑”
[b fly];
}
return 0;
}
从上面的程序可以看到执行[b fly]时,不再执行Bird类的fly方法,而是执行Ostrich类的fly方法
这种子类包含父类方法同名的现象被称为方法重写,也被称为方法覆盖
方法重写必须注意方法签名关键字要完全相同,也就是方法名和方法签名中的形参标签都需要完全相同,否则就不能算方法重写。
(3)super关键字
super是Objective-C提供的一个关键字,super用于限定该对象调用它从父类继承得到的属性或方法
如果需要在子类方法中调用父类被覆盖的方法,则可以使用super关键字来调用父类被覆盖的方法。我们可以对上面的Ostrich类中添加一个方法,在这个方法中调用Bird被覆盖的fly方法,该方法代码如下:
- (void)Method {
//在子类方法中通过super显式调用父类被覆盖的fly方法
[super fly];
}
通过Method方法的帮助,就可以让Ostrich对象既可以调用自己重写的fly方法,也可以调用Bird类中被覆盖的fly方法(调用Method方法即可实现)
super既可出现在类方法中,也可以出现在实例方法中。在类方法中使用super调用父类的方法时,被调用的父类方法只能是类方法;在实例方法中使用super调用父类的方法时,被调用的父类方法只能是实例方法。
当子类继承父类时,子类可以获得父类中定义的成员变量,因此,子类接口部分不允许定义与父类接口部分重名的成员变量。
例如:
//如果在子类接口这样定义与父类接口部分重名的成员变量,程序就会报错
//父类接口部分代码如下:
#import <Foundation/Foundation.h>
@interface Base: NSObject {
@private
int _a;
}
@end
//子类接口部分代码如下:
#import <Foundation/Foundation.h>
#import "Base.h"
@interface SubClass: Base {
int _a;//与父类接口部分成员变量重名
}
@end
因此,无论父类接口部分的成员变量使用何种访问控制符限制,子类接口部分定义的成员变量都不允许与父类接口部分定义的成员变量重名。
而在类实现部分定义的成员变量将被限制在该类的内部,因此,父类在类实现部分定义的成员变量对子类没有任何影响。无论是接口部分还是实现部分,子类定义的成员变量都可以与父类实现部分定义的成员变量同名。
反过来也一样,在子类实现部分定义的成员变量也不受父类接口部分定义的成员变量的影响。也就是说,即使在父类的接口部分定义了名为 _a 的成员变量,子类的实现部分依然可以定义名为 _a的成员变量。
当子类实现部分定义了与父类重名的成员变量时,子类的成员变量就会隐藏父类的成员变量。因此,子类方法很难直接访问到父类的成员变量,此时可通过调用父类的方法来访问父类
下面我们可以创造一种情况,代码如下:
//父类接口部分代码如下:
#import <Foundation/Foundation.h>
@interface Parent: NSObject {
int _a;
}
- (void) setA: (int)a;//setter
- (int) a;//getter
@end
//父类Parent的实现部分如下:
#import <Foundation/Foundation.h>
#import "Parent.h"
@implementation Parent
- (void) setA: (int)a {
_a = a;
}
- (int) a {
return _a;
}
//重写init初始化
- (id) init {
if (self = [super init]) {
self -> _a = 5;
}
return self;
}
@end
//接着定义一个Parent的子类,该子类的接口部分代码如下:
#import <Foundation/Foundation.h>
#import "Parent.h"
@interface Sub: Parent
- (void) accessOwner;
@end
//接着写子类Sub的实现部分,该实现部分会定义一个名为_a的成员变量,该成员变量会隐藏父类的成员变量
#import <Foundation/Foundation.h>
#import "Sub.h"
@implementation Sub {
//该成员变量会隐藏父类的成员变量
int _a;
}
//重写init初始化
- (id) init {
if (self = [super init]) {
self -> _a = 7;
}
return self;
}
- (void) accessOwner {
//直接访问的是当前类中的成员变量
NSLog(@"子类中_a成员变量的值为:%d", _a);
//访问父类中被隐藏的成员变量
NSLog(@"父类中被隐藏的_a成员变量的值为:%d", super.a);
}
@end
//下面是主函数部分:
#import <Foundation/Foundation.h>
#import "Sub.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
Sub* c = [[Sub alloc] init];
[c accessOwner];
}
return 0;
}
最终的运行结果为:
子类中_a成员变量的值为:7
父类中被隐藏的_a成员变量的值为:5
从上方的代码中可以看出程序通过 super 关键字强制指定调用父类的 _a 属性(实际上就是获取 getter 方法返回值),通过这种方式可以访问到父类中被隐藏的成员变量。
从上面的运行结果可以看到,在子类实现部分定义与父类同名的成员变量,只是隐藏父类的成员变量,虽然程序只是创建了一个 Sub 对象,但该对象内部依然有两块内存来保存 a成员变量,其中一块内存保存父类中被隐藏的 _a 成员变量,可以通过父类中定义的方法来访问; 另一块内存保存子类实现部分定义的 _a 成员变量,可以在子类方法中直接访问。