面向对象—封装

一、面向对象和封装

面向对象的三大特性:封装(成员变量)、继承和多态

在OC语言中,使用@interface和@implementation来处理类。

17007345045400.jpg

@interface就好像暴露在外面的时钟表面,像外界提供展示以及接口。

@implementation就好像隐藏在时钟内部的构造实现,把具体的实现封装了起来。

二、Set方法

在开发过程中,考虑到安全性要求,我们一般不在成员变量名前面使用@public@protected等关键字修饰,而是使用Set方法来为对象提供成员变量的值。在set方法的内部也可以对一些不合理的赋值进行筛选过滤。

Set方法的作用:为外界提供一个设置成员变量值的方法

命名规范:

  1. 方法名必须以set开头
  2. Set后面跟上成员变量的名称,首字母大写
  3. 返回值一定是void
  4. 一定要接收一个参数,而且参数类型需要和成员变量的类型一致
  5. 形参名不能和成员变量名一样(苹果官方推荐成员变量名前加_以示区分)

Set方法的好处:

  1. 不让数据暴露在外,保证了数据的安全性
  2. 对设置的数据进行过滤

Set方法使用示例:

Set方法的声明:

@interface Person : NSObject
{
// 这里不再使用@public关键字
int _age;
}

//set方法的声明,接受一个参数
- (void)setAge:(int)age;
- (void)print;

@end

Set方法的实现:

@implementation Person

// set方法的实现
- (void)setAge:(int)age
{
    _age = age;
}

- (void)print
{
    NSLog(@"age = %d", _age);
}
@end

测试程序:

Person *person = [[Person alloc] init];

// 使用set方法设置Person类对象age变量的值为10
[person setAge:10];
[person print];


// 打印结果
2016-07-12 14:08:39.406 test[68504:10912801] age = 10

三、Get方法

Get方法的作用:为调用者返回对象内部的成员变量

命名规范:

  1. 一定有返回值,返回值的类型和成员变量的类型一致
  2. 方法名和成员变量名一样
  3. 不需要接收任何参数

Get方法使用示例:

Get方法的声明:

@interface Person : NSObject
{
    NSString *_name;
}

- (void)setName:(NSString *)name;
- (NSString *)name;

@end

Get方法的实现:

@implementation Person

// set方法的实现
- (void)setName:(NSString *)name
{
    _name = name;
}

- (NSString *)name
{
    return _name;
}

@end

测试程序:

Person *person = [[Person alloc] init];

[person setName:@"xiaoming"];

NSLog(@"%@", person.name);

// 打印结果
2016-07-12 14:19:32.702 test[68694:10983407] xiaoming
注意1:在实际的开发中,不一定set和get方法都会提供,如果内部的成员变量比如学生的学号这样的数据只允许外界读取,但是不允许修改的情况,则通常只提供get方法而不提供set方法。 注意2:成员变量名的命名以下划线开头,get方法名不需要带下划线,使用下划线开头有两个好处:(1)与get方法的方法名区分开来;(2)可以和一些其他的局部变量区分开来,下划线开头的变量,通常都是类的成员变量。

四、Self关键字

Self是一个指针,谁调用了当前方法,self就指向谁

【出现在对象方法中,就代表着当前对象,出现在类方法中,就代表着当前类】

Self的用途:

  1. 可以利用self->成员变量名访问当前对象内部的成员变量(仅在对象方法中)
  2. [self 方法名];可以调用其他的对象方法或者是类方法。

五、练习

要求:设计一个成绩类

/*
设计一个成绩类,
这个成绩类有以下四个属性:

1. OC成绩(可读写)
2. C成绩(可读写)
3. 总分(只读)
4. 平均分(只读)
*/
@interface Score : NSObject
{
int _OCScore;
int _CScore;
int _sum;
int _average;
}

- (void)setOCScore:(int)ocScore;
- (int)ocScore;
- (void)setCScore:(int)ocScore;
- (int)cScore;

- (int)sum;
- (int)average;

@end

实现:

@implementation Score

- (void)setOCScore:(int)ocScore
{
  _OCScore = ocScore;
  _sum = _OCScore + _CScore;
  _average = _sum * 0.5;
}
- (int)ocScore
{
  return _OCScore;
}
- (void)setCScore:(int)ocScore
{
  _CScore = ocScore;
  _sum = _OCScore + _CScore;
  _average = _sum * 0.5;
}
- (int)cScore
{
  return _CScore;
}
- (int)sum
{
  return _sum;
}
- (double)average
{
  return _average;
}

@end

测试程序:

NSLog(@"sum = %d", [s sum]);
NSLog(@"average = %.2f", [s average]);

[s setCScore:100];
NSLog(@"sum = %d", [s sum]);
NSLog(@"average = %.2f", [s average]);

// 打印结果
2016-07-12 14:42:08.041 test[69041:11083543] sum = 178
2016-07-12 14:42:08.041 test[69041:11083543] average = 89.00
2016-07-12 14:42:08.041 test[69041:11083543] sum = 180
2016-07-12 14:42:08.041 test[69041:11083543] average = 90.00

面向对象—继承

一、基本概念

程序的世界和人类的“对象”世界在思想上是没有设么区别的,富二代继承了父母,自然就拥有了父母拥有的所有资源,子类继承了父类同样就拥有了父类所有的方法和属性(成员变量)。

在这里动物是猫类和狗类的父类,黑猫和白猫类是猫类的子类。

继承的好处:

  1. 抽取出了重复的代码
  2. 建立了类和类之间的联系

继承的缺点: 耦合性太强

二、OC中的继承

@interface Animal:NSObject
//动物里继承了NSObject,获得NSObject类的方法;
@end
 
@interface Dog :Animal
//dog类继承Animal类
@end
注意:OC语言是单继承语言。在oc语言中,基本上所有类的根类都是NSObject类。

三、继承的使用注意

  1. 编译器从上往下执行,所以在子类前面至少应该要有父类的声明;
  2. OC中不允许子类和父类拥有相同名称的成员变量名;
  3. OC中的子类可以拥有和父类相同名称的方法,在子类调用时,优先去自己的内部寻找,如果没有则一层一层的往上找;
提示:重写即子类重新实现了父类中的某个方法,覆盖了父类以前的实现。

示意图:一共有三个类,Person类继承了NSObject类,Student类继承了Person类。

创建一个Student *s=[[Student alloc] init];

此时会把Student类和这个类的父类加载进内存。

提示:每个类中都有一个super class指针,该指针指向自己的父类。对象中有一个isa指针,该指针指向调用该对象的类。

四、继承和组合

继承的适用场合:

  1. 当两个类拥有相同的属性和方法时,就可以将相同的属性和方法抽取到一个父类中。
  2. 当A类完全拥有B类中的部分属性和方法时,可以考虑让B类继承A类(考虑),在这种情况下,也可以考虑使用组合。

继承:***是xxx,如狗是动物,可以让狗继承动物类

组合:***拥有xxx,如学生有书,可以让书这个类作为学生类的属性

五、关键字super

Super关键字,在子类中重写方法时,可以让调用者跳过这一层而调用父类中的方法。

作用:

  1. 直接调用父类中的某一个方法
  2. Super处在对象方法中,那么就会调用父类的对象方法;super处于类方法中,那么就会调用父类的类方法。

使用场景:子类在重写父类方法时,想要保留父类的一些行为。

面向对象—多态

一、基本概念

多态在代码中的体现,即为多种形态,必须要有继承,没有继承就没有多态。

在使用多态是,会进行动态检测,以调用真实的对象方法。

多态在代码中的体现即父类指针指向子类对象。

Animal类的声明

@interface Animal : NSObject

// Animal 类中声明一个 eat 的对象方法
- (void)eat;
@end

Animal类的实现

@implementation Animal

- (void)eat
{
    NSLog(@"动物吃东西!");
}

@end

Dog类继承自Animal类

// Dog 类继承自Animal类,拥有了 Animal 类中的所有属性和方法
@interface Dog : Animal

// 在子类中声明了弗雷已经有的 eat 方法,这称之为方法的重写

- (void)eat;

@end

Dog类的实现

@implementation Dog

- (void)eat
{
    NSLog(@"狗吃东西!");
}

@end

测试程序:

// Dog 类型的指针指向 Dog 类型的对象
  Dog *dog = [[Dog alloc] init];

  // Dog 类对象调用对象方法
  [dog eat];

  // 多态
  // 父类指针指向子类对象
  Animal *a = [[Dog alloc] init];

  // 请问这里调用的是哪个方法?
  [a eat];
  // 动态监测-调用方法时会检测对象的真实类型

  // Animal类型的指针指向Animal类型的对象
  Animal *a1 = [[Animal alloc] init];
  [a1 eat];

  // 下面都是多态的体现
  NSObject *n = [[Dog alloc] init];
  // [n eat]; 不能这样调用

  NSObject *n1 = [[Animal alloc] init];


  // 打印结果
  2016-07-12 19:55:05.783 test[70257:11992740] 狗吃东西!
  2016-07-12 19:55:05.783 test[70257:11992740] 狗吃东西!
  2016-07-12 19:55:05.783 test[70257:11992740] 动物吃东西!

二、使用注意

代码分析:

Dog *d=[[Animal  alloc] init];   //动物是一条狗?语义正确吗?
NSString *str=[Dog  new];     //狗是一个字符串?正确吗?

OC语言是一门弱语法的语言,编译的时候并不会报错,所以这就要求我们在实际的开发过程中一定要按照既定的规范来写代码,不要出现狗是一个字符串这样的问题。

多态的好处:需要一个新的函数专门用来喂狗

void feed(Dog *d)
{
    [d  eat];
}

如果这个时候也需要喂猫,那就应该重写新一个新的函数

void feed(Cat *c)
{
    [c  eat];
}

而狗和猫实际上都继承自动物这个类,在这里就可以使用多态来简化代码了。

这里只需要把函数的参数写成是Animal *类型的,那么Dog和Cat类型的对象就都可以传入进来。

调用的时候直接改变参数就可以了。

多态的局限性:父类类型的指针变量不能直接调用子类特有的方法。

不建议的做法:

Animal *a=[[Dog alloc] init];
[a run];//在Animal类中没有run方法,这里调用了狗对象的方法。

解决方法:可以将a强制转换为Dog*类型的变量,如下:

Dog *d=(Dog *)a;//使用强制转换,这里a和d指向的是同一个狗对象

三、多态使用总结

  1. 没有继承就没有多态
  2. 代码的体现:父类类型的指针指向子类对象
  3. 好处:如果函数方法参数中使用的是父类类型,则可以传入父类和子类对象,而不用再去定义多个函数来和相应的类进行匹配了。
  4. 局限性:父类类型的变量不能直接调用子类特有的方法,如果必须要调用,则必须强制转换为子类特有的方法。

四、字符串补充内容

@“234”字符串也是一个对象,属于NSString这个类。下面是对字符串对象的一些代码说明:

// 最简单的创建字符串的方式
NSString *str = @"xiaoming";
// 使用C字符串的方式创建字符串
char *name = "mingxiao";

// 打印字符串
NSLog(@"%s", name);
NSLog(@"%@在敲代码练习呢!", str);

int a = 5;
int b = 10;
// 创建字符串的另外一种方法
NSString *str1 = [NSString stringWithFormat:@"我已经敲了%d个小时,准备学够%d个小时", a, b];
NSLog(@"%@", str1);

// 打印结果
2016-07-12 20:08:00.171 test[70395:12064646] mingxiao
2016-07-12 20:08:00.171 test[70395:12064646] xiaoming在敲代码练习呢!
2016-07-12 20:08:00.171 test[70395:12064646] 我已经敲了5个小时,准备学够10个小时

字符串对象的length方法:计算的是字符串的字数,而不是像strlen函数那样,计算的是字符数。如“哈ha123” length得出的结果是6,返回unsigned long类型,而strlen函数得出的结果是8,因为一个汉字占3个字节。

提示:字数也包括空格。

NSString *str = @"xiaoming love read";
NSString *str1 = @"哈哈123";

// 请注意方法的返回值类型,这里使用 %d 打印
// legth 方法, 获取当前字符串的字数
NSLog(@"字数为%ld", [str length]);
NSLog(@"字数为%ld", [str1 length]);

// 打印结果
2016-07-12 20:10:41.136 test[70446:12072597] 字数为18
2016-07-12 20:10:41.137 test[70446:12072597] 字数为5

点语法和变量作用域

一、点语法

(一)认识点语法

声明一个Person类:

#import <Foundation/Foundation.h>

@interface Person : NSObject
{
    int _age; //默认为@protected
}
- (void)setAge:(int)age;
- (int)age;
@end

Person类的实现:

#import"Person.h"
 
@implementation Person
- (void)setAge:(int)age
{
    _age = age;// 不能写成self.age = newAge,相当与 [self setAge:newAge];
}

- (int)age  //get方法
{
    return _age;
}

@end

点语法的使用:

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

main(int argc, constchar * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];
        //[person setAge:10];
        person.age = 10;//点语法,等效与[person setAge:10];
        //这里并不是给person的属性赋值,而是调用person的setAge方法
        //int age = [person age];
        int age = person.age;//等效与int age = [person age]
        NSLog(@"age is %i", age);
        [person release];
    }
return0;
}

(二)点语法的作用

OC设计点语法的目的,是为了让其他语言的开发者可以很快的上手OC语言开发,使用点语法,让它和其他面向对象的语言如java很像。

(三)点语法的本质

点语法的本质是方法的调用,而不是访问成员变量,当使用点语法时,编译器会自动展开成相应的方法。切记点语法的本质是转换成相应的set和get方法,如果没有set和get方法,则不能使用点语法。

如:

Stu.age = 10; 展开为:[stu setAge:10];

int a = stu.age; 展开为:[stu age];

编译器如何知道是set方法还是get方法?主要是看赋值(可以使用断点调试来查看)。

在OC中访问成员变量只有一种方式即使用-> 如stu->age,这种情况要求在@public的前提下。

(四)点语法的使用注意

下面的使用方式是一个死循环:

  1. 在set方法中,self.age=age;相当于是[self setAge:age];
  2. 在get方法中,return self.age;相当于是[self age];

二、变量作用域

(一)变量的作用域主要分为四种:

  1. @public(公开的)在有对象的前提下,任何地方都可以直接访问。
  2. @protected(受保护的)只能在当前类和子类的对象方法中访问。
  3. @private (私有的)只能在当前类的对象方法中才能直接访问。
  4. @package(框架级别的)作用域介于私有和公开之间,只要处于同一个框架中就可以直接通过变量名访问

(二)使用注意和补充

  1. 在类的实现即.m文件中也可以声明成员变量,但是因为在其他文件中通常都只是包含头文件而不会包含实现文件,所以在这里声明的成员变量是@private的。在.m中定义的成员变量不能和它的头文件.h中的成员变量同名,在这期间使用@public等关键字也是徒劳的。
  2. @interface @end之间声明的成员变量如果不做特别的说明,那么其默认是protected的。
  3. 一个类继承了另一个类,那么就拥有了父类的所有成员变量和方法,注意所有的成员变量它都拥有,只是有的它不能直接访问。

@property @synthesize和id

一、@property @synthesize关键字

注意:这两个关键字是编译器特性,让xcode可以自动生成getter和setter的声明和实现。

(一)@property 关键字

@property关键字可以自动生成某个成员变量的setter和getter方法的声明
@property int age;

编译时遇到这一行,则自动扩展成下面两句:

- (void)setAge:(int)age;
- (int)age;

(二)@synthesize关键字

@synthesize关键字帮助生成成员变量的setter和getter方法的实现。
语法:@synthesize age=_age;
相当于下面的代码:

- (void)setAge:(int)age
{
  _age = age;
}
- (int)age
{
  return _age;
}

(三)关键字的使用和使用注意

类的声明部分:

@interface Person : NSObject

// 使用 @property 关键字实现 _age成员变量的 set 和get 方法的声明
@property int age;

@end

类的实现部分:

@implementation Person

// 使用 @synthesize 关键字实现 _age 成员变量的 set 和 get 方法的声明
@synthesize age = _age;
// 左边的 age 代表要把 @property int age 实现
// 右边的 _age 代表着访问 _age 这个成员变量

@end

测试程序:

Person *p = [[Person alloc] init];
[p setAge:10];

// 使用点语法, 打印成员变量的值以验证
// 点语法的本质是调用 set 和 get 方法
NSLog(@"age = %d", p.age);

[p setAge:22];

NSLog(@"age = %d", p.age);

// 打印结果
2016-07-30 00:00:17.353 test[28024:6636559] age = 10
2016-07-30 00:00:17.353 test[28024:6636559] age = 22

新版本中:

类的声明部分:

@interface Person : NSObject
{
    // 类的成员变量, 这样的方式默认为 protected
    // 如果这里不写成员变量, 那么会自动生成, 但是 private 的
    int _age;
    double _weight;
    double _height;
    NSString *_name;
}
// 使用 @property 关键字生成成员变量的 set 和get 方法的声明和实现
@property int age;
@property double weight;
@property double height;
@property NSString *name;

@end

类的实现部分:

@implementation Person

// 从 xcode4.4 以后, @property 关键字独揽了三个功能

@end

测试程序:

// person 类型的指针指向一个 person 类型创建的对象
   Person *p = [[Person alloc] init];

   // 使用点语法为 _age 成员变量赋值
   p.age = 22;
   // 使用set方法为成员变量赋值
   [p setHeight:169];
   [p setWeight:50];
   [p setName:@"yongyuan"];

   // 使用点语法,本质是调用 get 方法
   NSLog(@"age = %d, height = %f, weight = %f, name = %@", p.age, p.height, p.weight, p.name);

   // 打印结果
   2016-07-30 00:09:49.008 test[28138:6663258] age = 22, height = 169.000000, weight = 50.000000, name = yongyuan
  1. 在老式的代码中,@property只能写在@interface @end中,@synthesize只能写在@implementation @end中,自从xcode 4.4 后,@property就独揽了@property@synthesize的功能。
  2. @property int age;这句话完成了3个功能,注意:这种方式生成的成员变量是private的:
    1. 生成_age成员变量的getset方法的声明;
    2. 生成_age成员变量setget方法的实现;
    3. 生成一个_age的成员变量。
  3. 可以通过在{}中加上int _age;显示的声明_ageprotected的。
  4. 原则:getset方法同变量一样,如果你自己定义了,那么就使用你已经定义的,如果没有定义,那么就自动生成一个。
  5. 手动实现:
    1. 如果手动实现了set方法,那么编译器就只生成get方法和成员变量;
    2. 如果手动实现了get方法,那么编译器就只生成set方法和成员变量;
    3. 如果setget方法都是手动实现的,那么编译器将不会生成成员变量。
@interface Dog : NSObject
{
    // 可以省略这里的成员变量
    int _age;
}

// 使用 @property 关键字生成成员变量 set 和 get 方法的声明和实现
@property int age;

@end

@implementation Dog

// 自定义的成员变量的放回方法

- (int)age
{
  return 10;
}
@end

 Dog *d = [[Dog alloc] init];

 // 使用点语法调用 set 方法设置 ahe 的值为5
 // 在这里调用 set 方法, 因为没有, 所以编译器在编译时自动生成了
 d.age = 5;

 // 因为自己定义了 get 方法, 所以 xcode 尊重用户的选择, 请注意输出结果

 NSLog(@"%d", d.age);

// 打印结果
2016-07-30 00:19:52.004 test[28251:6708346] 10

二、id

id 是一种类型,万能指针,能够指向操作任何的对象。

注意:在id的定义中,已经包好了*号。id指针只能指向 OC 的对象。

id 类型的定义

Typedef struct objc object
{
    Class isa;
} *id;

局限性:调用一个不存在的方法,编译器会马上报错。

构造方法

一、构造方法

(一)构造方法的调用

完整的创建一个可用的对象:Person *p = [Person new];

new方法的内部会分别调用两个方法来完成2件事情。

  1. 使用alloc方法来分配存储空间(返回分配的对象);
  2. 使用init方法来对对象进行初始化。

可以把new方法拆开如下:

  1. 调用类方法+alloc分配存储空间,返回未经初始化的对象 : Person *p1=[person alloc];
  2. 调用对象方法-init进行初始化,返回对象本身 : Person *p2=[p1 init];
  3. 以上两个过程整合为一句:Person *p=[[Person alloc] init];

说明:init方法就是构造方法,是用来初始化对象的方法,注意这是一个对象方法,一减号开头。默认初始化完毕后,所有成员变量的值都为0。

(二)构造方法的代码示例

需求1,如果我需要让每个对象创建出来的初始值是10,而不是1,应该怎么办呢?

// Person 类的声明
@interface Person : NSObject

@property int age;

@end

/// Person类的实现
@implementation Person

// 重写init方法

- (instancetype)init
{
    // 1. 初始化对象拥有的父类成员变量
    self = [super init];
    if (self)
    {
        // 2. 初始化对象自有得成员变量
        _age = 10;
    }
    // 3. 返回一个已经初始化完成的对象
    return self;
}

@end

Person *p = [[Person alloc] init];
NSLog(@"%d", p.age);

// 打印结果
2016-07-30 00:31:46.785 test[28361:6773695] 10

需求2,让学生继承人类,要求学生对象初始化之后,年龄是10,学号是1,怎么办?

@interface Student : Person

@property int no;

@end

@implementation Student

- (instancetype)init
{
    // 1. 初始化对象拥有的父类成员变量, 调用父类的构造方法, 把年龄设置为10
    self = [super init];
    if (self)
    {
        // 2/初始化对象自有得成员变量, 设置no的值为1
        _no = 1;
    }
    // 3. 返回一个已经初始化完成的对象
    return self;
}

@end

Student *s = [[Student alloc] init];
NSLog(@"%d, %d", s.age, s.no);

// 打印结果
2016-07-30 00:37:22.160 test[28436:6799197] 10, 1

(三)构造方法使用注意

  1. 子类拥有的成员变量包括自己的成员变量以及从父类继承而来的成员变量,在重写构造方法的时候应该首先对从父类继承而来的成员变量先进行初始化。
  2. 原则:先初始化父类的,再初始化子类的。
  3. 重写构造方法的目的:为了让对象方法一创建出来,成员变量就会有一些固定的值。
  4. 注意点:#1先调用父类的构造方法[super init]; #2再进行子类内部成员变量的初始化。

二、自定义构造方法

(一)自定义构造方法的规范

  1. 一定是对象方法,以减号开头
  2. 返回值一般是id类型
  3. 方法名一般以initWith开头

(二)自定义构造方法的代码实

Person类的声明,其中声明了两个接收参数的自定义构造方法

 // Person 类的声明
@interface Person : NSObject

@property NSString *name;
@property int no;

- (id)initWithName:(NSString *)name;

- (id)initWithName:(NSString *)name andNo:(int)no;

@end

Person类的实现

/// Person类的实现
@implementation Person
// 自定义构造方法

// 接受1个参数的构造方法
- (id)initWithName:(NSString *)name
{
    if (self = [super init])
    {
        _name = name;
    }
    return self;
}

// 接受2个参数的构造方法
- (id)initWithName:(NSString *)name andNo:(int)no
{
    if (self = [super init])
    {
        _name = name;
        _no = no;
    }
    return self;
}

@end

Student继承自Person类,声明了一个接收三个参数的构造方法

@interface Student : Person

@property int age;

- (id)initWithName:(NSString *)name andNo:(int)no andAge:(int)age;

@end

Student类的实现

// 学生类方法的实现
// 一个接受三个参数的自定义构造方法
@implementation Studen

- (id)initWithName:(NSString *)name andNo:(int)no andAge:(int)age
{
    if (self = [super initWithName:name andNo:no])
    {
        // 初始化自己独有的成员变量
        _age = age;
    }
    return self;
}

@end

测试主程序

// 调用接受一个参数的自定义构造函数
Person *p = [[Person alloc] initWithName:@"xiaomi"];
NSLog(@"%@", p.name);

// 调用接受两个参数的自定义构造函数
Person *p2 = [[Person alloc] initWithName:@"xiaoming" andNo:1];
NSLog(@"%@ %d", p2.name, p2.no);

// 学生类调用接受三个参数的构造函数
Student *s = [[Student alloc] initWithName:@"xiaohong" andNo:3 andAge:4];
NSLog(@"%@,%d,%d", s.name, s.no, s.age);

// 打印结果
2016-07-30 00:52:40.865 test[28632:6861736] xiaomi
2016-07-30 00:52:40.866 test[28632:6861736] xiaoming 1
2016-07-30 00:52:40.866 test[28632:6861736] xiaohong,3,4

(三)自定义构造方法的使用注意

  1. 自己做自己的事情
  2. 父类的方法交给父类的方法来处理,子类的方法处理子类自己独有的属性