OC-基础总结(一)

0.872字数 7544阅读 3536

OC基础总结

重新回过头看这些基础知识,对许多知识点都有新的认识,拥有坚实的基础才能更快的成长。

#improt

OC程序的源文件的后缀名是.m m代表message表示消息机制。main 仍然是OC程序的入口和出口,main函数有一个int类型的返回值,代表程序的结束状态。

#import预处理指令,是#inlcude指令的增强版,作用是将文件的内容在预编译的时候拷贝到写指令的地方。 #import做了优化,同一个文件无论#import多少次,都只会包含一次。
简要原理:#import指令在包含文件的时候,底层会先判断这个文件是否被包含,如果包含过就会略过。

因此#import #include主要区别在于使用#include需要处理重复引用,而#import能防止同一个文件被多次包含,不需要处理重复引用。

框架

  1. 一个功能集苹果或者第三方事先将一个协成员在开发程序的时候经常要用到的功能事先写好。把这些功能封装在一个类或者函数中,这些函数和类的集合就叫做框架。
  2. Foundation框架
    Foundation:基础框架,这个框架中提供了一些最基础的功能,输入和输出,一些数据类型。

如何使用面向对象来设计程序

面向对象后期维护和修改十分方便。当我们遇到一个需求的时候,不要亲自去实现。

  1. 先看看有没有现成的人是专门做这件事情的,框架,如果有直接使用。
  2. 如果没有就自己造一个拥有这样功能的对象,并且创造出来的这个对象可以多次被使用。

类和对象

  1. 对象 - 具体
    对象是现实生活中一个具体存在,看得见,摸得着,拿过来就可以直接使用
  2. 类 - 统称
    物以类聚,人以群分。
    类时对一群具有相同特征或者行为的事物的一个统称,抽象的,不能直接使用,如果非要使用类的话,只能去类中找到类的具体存在,也就是对象,然后使用。

类和对象的关系

类是模板,类的对象是根据这个模板创建出来的,类模板中有什么,对象中就有什么,绝不可能多,也绝不可能少。

如何设计一个类

设计类的三要素

  1. 类的名字。
  2. 这类事物具有的相同的特征,这类事物用手什么。
  3. 这类事物的能干什么。

类加载

  1. 在创建对象的时候,肯定是需要访问类的。
  2. 声明一个类的指针变量也会访问类的。

在程序运行期间,当某个类第一次被访问到的时候,会将这个类存储到内存中的代码段区域,这个过程叫做类加载。

只有类在第一次被访问的时候,才会做类加载,并且一旦类被加载到代码段以后,直到程序结束的时候才会被释放。

对象在内存中究竟是如何存储的。

例:Person *p1 = [Person new];

  1. Person *p1; 会在栈内存中申请一块空间,在栈内存中声明1个Person类型的指针变量p1。p1是一个指针变量,那么只能存储地址。
  2. [person new];真正在内存中创建对象的其实是这句代码。
  3. new方法在堆内存中创建一块合适大小的空间,然后在空间中根据类的模板创建对象。
    类模板中定义了什么属性,就把这些属性依次声明在对象之中。
    对象中还有另外一个属性,叫做isa ,是一个指针,指向对象所属的类在代码段中的地址。
  4. 初始化对象的属性,给对象的属性赋默认值。
    如果属性的类型是基本数据类型,那么就赋值为0
    如果属性的类型是c语言的指针类型,那么就赋值为NULL
    如果属性的类型为OC语言的类指针类型,那么就赋值为nil
  5. 注意
    1). 对象中只有属性没有方法,属性包括自己类的属性,外加一个isa指针指向代码段中的类。
    2). 如何访问对象的属性,指针名->属性名
    根据指针,找到指针指向的对象,在找到对象中的属性来访问。
    3). 如何调用方法。[指针名 方法名];
    先根据指针名找到对象,对象发现要调用方法,在根据对象的isa指针找到类。然后调用类里的方法。
    4). 为什么不把方法存储到对象之中。
    因为每一个对象的方法的代码实现都是一模一样的,没有必要为每一个对象都保存一个方法,这样的话就太浪费空间了,既然都一样,那么就只保存一份在代码段中。
    5). 对象属性是有默认值的。

nil与NULL的区别

NULL 可以作为指针变量的值,如果一个指针变量的值是NULL值代表这个指针不指向内存中的任何一块空间,其实等价于0。NULL其实是一个宏,就是0。

nil 只能作为指针变量的值,代表这个指针变量不指向内存中任何空间。nil其实也等价于0,也是一个宏,就是0。

所以NULL和nil其实是一样的,虽然使用NULL的地方可以使用nil,但是不建议随意使用。C指针用NULL OC的类指针用nil。

Person *p1 = nil;表示p1指针不指向任何对象。
如果一个指针的值为nil代表这个指针不指向任何对象,此时如果通过p1指针去访问p1指针指向的对象的属性,运行就会报错。如果通过p1指针去调用对象的方法,运行不会报错,但是方法不会执行。

多个指针指向同一个对象

同类型的指针变量之间是可以相互赋值的。p1,p2指向同一个对象,无论谁修改对象的属性都会修改。因为他们指向同一块内存空间。

对象和方法

对象可以作为方法的参数也可以作为方法的返回值。
类的本质是我们自定义的一种数据类型,并且对象在内存中的大小是由我们自己决定的,数据类型是在内存中开辟空间的一个模板

当对象作为方法的参数传递的时候,是地址传递。所以,在方法内部通过形参去修改形参指向的对象的时候,会影响实参变量指向的对象的值。对象作为方法的返回值,返回的是对象的地址

对象作为类的属性。

属性的本质是变量,在创建对象的时候,对象当中的属性是按照类模板中的规定逐个创建出来的。类模板中属性是什么类型,那么对象中的属性就是什么类型。
如果对象的属性是另外一个类的对象,这个属性仅仅是一个指针变量而已,并没有对象产生。这个时候还要为这个属性赋值一个对象的地址,才可以正常使用。
类模板中属性是什么类型,对象当中的属性就是什么类型。

类方法的声明和调用

类方法的调用不依赖对象,如果要调用类方法不需要去创建对象。而是直接使用类名就可以调用类方法。

类方法和对象方法的调用过程。

类方法和对象方法的调用过程

类方法节约空间且效率高,因为调用类方法不需要创建对象。但是在类方法中不能直接访问属性。因为属性只有在对象创建的时候才会创建在对象之中,而类方法在执行的时候有可能还没有类对象,所以不能访问属性。但是我们可以在类方法中创建类对象。

同理,在类方法中也不能通过self直接调用当前类的其他的对象方法,因为对象方法只能通过对象来调用,在对象方法中可以直接调用类方法。

因此如果方法不需要直接访问属性,也不需要直接调用其他的对象方法,那么我们就可以直接将这个方法定义为类方法。

继承

  1. 子类从父类继承,就意味着子类拥有了父类的所有成员,包括属性和方法。
  2. 继承是类在继承,而不是对象在继承,子类对象中拥有父类对象中的同样的成员。

如果不是所有的子类都拥有的方法,那么这个方法就不应该定义在父类之中,因为一旦定义在父类之中,那么所有的子类都拥有该方法

继承的特点

  1. 一个类只能有一个父类,不能有多个父类。
  2. 传递性,A继承B,B继承C 那么A就同时拥有B和C的成员。

NSObject

NSObject类是所有类的父类,NSObject类中包含了创建对象的方法,所以我们自己创建的类必须直接或者间接的继承自NSObject。
NSObject中有一个isa指针的属性,所以每一个子类对象中都有一个叫做isa的指针。

Super关键字

  1. 子类中已经有父类的属性,相当于子类中已经定义过父类的属性,因此子类当中不能存在和父类同名的属性,否则会出现冲突。
  2. 可以使用super关键字调用当前对象从父类继承过来的对象方法。可以使用self 也可以使用 super
  3. 类方法也可以被子类继承。子类可以直接调用父类的类方法。
  4. super只能用来调用父类的对象方法或者类方法,不能用来访问属性。
  5. 子类从父类继承,相当于子类模板中拥有了父类模板中的所有成员。
  6. 创建一个子类对象,仍然是根据子类模板来创建对象,只不过子类模板中拥有父类的属性和方法,也有子类自己的属性和方法。
  7. 父类的方法用super 可读性更高,我们很快就能知道这个方法是父类方法。

访问修饰符:

用来修饰属性,可以限定对象的属性在那一段范围之中访问。

@private : 私有,被其修饰的属性只能在本类的内部访问。
@protected: 受保护的 被其修饰的属性只能在本类以及本类的子类中访问。只能在本类和子类的方法实现中访问。
@package: 被其修饰的属性,可以在当前框架中访问。
@public: 公共的,被其修饰的属性可以在任意地方访问。

如果不为属性指定访问修饰符 默认:protected
子类仍然可以继承父类的私有属性。就算父类的属性是private,只不过在子类当中无法直接访问从父类继承过来的私有属性,可以通过set get方法来访问。

访问修饰符的作用域
从写访问修饰符的地方开始往下,直到遇到另外一个访问修饰符的或者结束大括弧为止,中间的所有的属性都应用这个访问修饰符。

使用建议
@public 无论什么情况下都不要使用,属性不要直接暴漏给外界。
@private 如果属性只想在本类中使用,不想再子类中使用。
@protected 如果你希望属性只在本类和本类的子类中使用。

访问修饰符只能用来修饰属性,不能用来修饰方法。

里氏替换原则

子类可以替换父类的位置,并且程序的功能不受影响。
即一个父类指针指向一个子类对象,可正常调用子类的方法和属性。

LSP 里氏替换原则:
一个指针中不仅可以存储本类对象的地址,还可以存储子类对象的地址
如果一个指针的类型是NSObject类型的,那么这个指针中可以存储任意的OC对象的地址。
如果一个数组的元素的类型是一个OC指针类型的,那么这个数组中不仅可以存储本类对象还可以存储子类对象。
如果一个数组元素是NSObject指针类型,那就意味着任意类型的对象都可以存在数组中。
如果一个方法的参数是一个对象,我们可以传本类对象也可以传子类对象
当一个父类指针指向1个子类对象的时候,通过父类指针就只能去调用子类对象中的父类成员。

方法重写

当子类拥有父类的行为,但是子类的行为与父类不同。这个时候就可以通过重写父类的方法来实现。

当一个父类指针指向一个子类对象的时候,通过这个父类指针调用的方法,如果子类对象中重写了这个方法,调用的就是子类重写的方法。

多态

指的是同一个行为,对于不同的事物具有完全不同的表现形式。同一个行为具备多种形态。子类重写父类的方法就是多态。

description 方法

description方法是定义在NSObject之中的。我们通过重写description方法来修改NSLog的输出形式。NSLog的底层就是description方法。

结构体与类的异同点

相同点:
都可以将多个数据封装为一个数据

不同点:

  1. 结构体只能封装数据,而类不仅可以封装数据还可以封装行为。
  2. 结构体变量分配在栈空间 (局部)
  3. 对象变量分配在堆空间
  4. 栈的特点:空间相对较小,但是存储在栈中的数据访问的效率更高一些
  5. 堆的特点:空间相对较大,但数据访问的效率相对要低。

如果表示的实体没有行为,只有属性。
那么如果属性比较少,只有几个,那么这个时候就定义为结构体,分配在栈,提高效率。如果属性比较多,不要定义成结构体,因为这样结构体变量会在栈中占据比较大的空间,导致访问效率降低。

类的本质

类是以Class对象存储在代码段中的

内存中的五大区域
栈 存储局部变量
堆 允许程序员自己申请的空间,需要程序员自己控制
BSS段 存储没有初始化的全局变量和静态变量
数据段 用来存储已经初始化的全局变量,静态变量还有常量
代码段 用来存储程序的代码。

类加载:当类第一次被访问的时候,这个类就会被加载到代码段存储起来。

  1. 类什么时候加载到代码段
    类第一次被访问的时候,类就会被加载到代码段存储,即类加载时。

  2. 类以什么样的形式存储在代码段。
    任何存储在内存中的数据都有一个数据类型,任何在内存中申请的空间也有自己的类型。那么在代码段存储类的那块空间是什么类型的?
    在代码段中存储类的步骤
    1). 先在代码段中创建一个Class对象,Class是Foundation框架中的一个类,这个Class对象就是用来存储类的信息的。
    2). 将类的信息存储在这个Class对象中
    所以类是以Class对象的形式存储在代码段的,存储类的这个Class对象也叫作类对象,所以存储类的类对象也有一个isa指针,指向存储父类对象。

  3. 类一旦被加载到代码段之后,什么时候会被释放。
    类一旦被加载到代码段之后是不会被回收的,除非程序结束。

  4. 如何拿到存储在代码段中的类对象?
    1). 调用类的类方法class就可以得到存储类的类对象的地址。
    2). 调用对象的对象方法 class就可以得到存储这个对象所属的类的class对象的地址。
    3). 对象中的isa指针的值其实就是代码段中存储类的类对象的地址。
    4). 拿到存储类的类对象以后,完全等价于类
    Class c1 = [Person class];c1对象就是Person类,c1完全等价于Person,可以使用类对象来调用类的类方法。
    注意:声明Class指针的时候,不需要加*因为在typedef的时候已经加了*

  5. 如何使用类对象
    1). 拿到存储类的类对象以后, Class c1 = [Person class]; c1对象就是Person类,c1对象完全等价于Person
    2). 使用类对象来调用类的类方法。
    3). 可以使用类对象来调用new方法,创建存储在类对象中的类的对象。
    4). 使用类对象,只能调用类的类方法,因为类对象就等价于存在其中的类

  6. 总结
    1). 类是以Class对象的形式存储在代码段之中的。
    2). 可以使用类对象来调用类的类方法。
    3). 通过class方法拿到存储类的类对象。

SEL选择器

SEL其实是一个类,要在内存中申请空间存储数据,SEL对象是用来存储一个方法的。
类是以Class对象的形式存储在代码段之中。那么如何将方法存储在类对象之中?

  1. 先创建一个SEL对象
  2. 将方法的信息存储在这个SEL对象之中
  3. SEL对象作为Class对象的一个属性来存储。
  4. 类似一个数组的形式将所有的SEL对象存储,即方法列表。SEL对象中存储方法的信息。

如何拿到存储方法的SEL对象

  1. 因为SEL是一个typedef类型的,在自定义的时候已经加了*所以我们声明SEL指针的时候不需要加*
  2. 取到存储方法的SEL对象 SEL sel = @selector(sayHi);
  3. 调用方法的本质:[p1 sayHi];
    1). 先拿到存储sayHi方法的SEL对象,也就是拿到存储sayHi方法的SEL数据,SEL消息。
    2). 将SEL消息发送给p1对象。
    3). p1对象接收到SEL消息以后,就知道要调用方法
    4). 根据对象的isa指针找到存储类的类对象。
    5). 找到这个类对象以后,在这个类对象中去搜寻是否有和传入的SEL数据相匹配的方法。如果有就执行,如果没有再找父类,直到NSObject。

OC最重要的1个机制:消息机制,调用方法的本质其实就是为对象发送SEL消息,[p1 sayHi];表示为p1对象发送1条sayHi消息。

手动的为对象发送SEL消息

  1. 先得到方法的SEL数据。
  2. 将这个SEL消息发送给p1对象。
    通过方法- (id)performSelector:(SEL)aSelector;手动发送消息
    Person *p1 = [Person new];
    SEL s1 = @selector(sayHi);
    [p1 performSelector:s1]; 与 [p1 sayHi]效果是完全一样的.
// 带一个参数和两个参数的方法
// 如果有多个参数可以把参数封装在一个对象里面,传递对象
 - (id)performSelector:(SEL)aSelector withObject:(id)object;
 - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

点语法

使用点语法访问对象的属性。

语法:对象名.去掉下划线的属性名
p.name = @“jack”;
这个时候就会将@“jack”赋值给p对象的_name属性。

原理:
本质并不是把@"jack"直接赋值给p对象的_name属性,实质上是调用set方法。

当使用点语法赋值的时候,编译器会将点语法转换为调用setter方法的代码。
当使用点语法取值的时候,编译器会将点语法转换为调用getter方法的代码。

在getter setter方法中慎用点语法,可能会造成无限递归而程序崩溃。
主要是看情况,get方法中如果点语法是调用set方法是可以使用的。
例如懒加载中我们是可以使用点语法为赋值的,因为懒加载是get方法,而赋值是调用set方法,所部不会递归调用。
如果属性没有封装setter getter 是无法使用点语法的。

@property

@property 自动生成getter setter 方法的声明。
原理:由编译器在编译的时候自动生成。

@synthesize
自动生成getter setter 方法的实现。
@synthesize age // age一定要是前面@property声明过的。

  1. 生成一个真私有属性,属性的类型和@synthesize对应的@property类型一致,属性的名字和@synthesize对应的@property名字一致。
  2. 自动生成setter方法的实现。实现的方式,将参数直接赋值给自动生成的那个私有属性。 self->age = age;
  3. 自动生成getter方法的实现。将生成的私有属性的值返回

希望@synthesize 不要生成私有属性,setter getter 的实现中操作我们已经写好的属性就可以了。

@synthesize @property名称 = 已经存在的属性名;
@synthesize age = _age;
  1. 不会再去生成私有属性
  2. 直接生成setter getter的实现 setter的实现:把参数的值直接赋值给指定的属性。getter实现:直接返回指定的属性的值。

@property增强。

Xcode 4.4之后,只要写一个@property 编译器会自动生成私有属性,并且自动生成getter setter 声明和实现。

@property NSString *name;

  1. 自动的生成一个私有属性,属性的类型和@property类型一致,属性的名称和@property的名称一致,属性的名称自动的加下划线。
  2. 自动生成这个属性的 setter getter方法的声明和实现。直接将参数的值赋值给自动生成的私有属性,直接返回生成的私有属性的值。

@property生成set get方法和成员属性,但是如果同时重写了setter getter方法,那么就不会自动生成私有属性了。需要自己写。
父类的property一样可以被子类继承,但是生成的属性是私有的,可以通过setter getter方法来访问

动态类型和静态类型

OC是一门弱语言,编译器在编译的时候,语法检查的时候没有那么严格。
强类型的语言:编译器在编译的时候,做语法检查的时候,就非常严格,行就是行,不行就是不行。

静态类型
指的是一个指针指向的对象是一个本类对象。
动态类型
一个指针指向的对象不是本类对象。

编译检查

编译器在编译的时候,检查能否通过一个指针去调用指针指向的对象的方法。

判断原则:看指针所属的类型之中有没有这个方法,如果有就认为可以调用,编译通过,如果这个类中没有,那么编译报错。这就叫做编译检查,在编译的时候,能不能调用对象的方法主要是看指针的类型,我们可以将指针的类型做转换,来达到骗过编译器的目的。

运行检查

编译检查只是骗过了编译器,但是这个方法究竟能不能执行,所以在运行的时候运行检查会去检查对象中是否真的有这个方法,如果有就执行,如果没有就报错

编译器,用编译器将C代码OC代码转化成二进制,编译器是个软件,苹果自己写的叫做LLVM,可以编译C OC Swift语言。

我们可以通过以下方法先来先判断以下对象中是否有这个方法,如果有再去执行,如果没有就别去执行,避免程序在没有方法的时候报错。

 1). 判断对象中是否有这个方法可以执行.
     - (BOOL)respondsToSelector:(SEL)aSelector; (最常用)     
 2). 判断类中是否有指定的类方法.
     + (BOOL)instancesRespondToSelector:(SEL)aSelector;
 3). 判断指定的对象是否为 指定类的对象或者子类对象.
     - (BOOL)isKindOfClass:(Class)aClass;
 4). 判断对象是否为指定类的对象 不包括子类.
     - (BOOL)isMemberOfClass:(Class)aClass;
 5). 判断类是否为另外1个类的子类.
    + (BOOL)isSubclassOfClass:(Class)aClass;

NSObject

OC中所有类的基类,根据LSP NSObject指针就可以指向任意的OC对象,所有NSObject指针是一个万能指针,可以指向任意的OC对象
缺点:如果要调用指向的子类对象的独有的方法,就必须要做类型转换。

id指针

是一个万能指针,可以指向任意的OC对象

  1. id是一个typedef自定义类型
  2. id指针,是1个万能指针,可以指向任意的OC对象。
    1). id是1个typedef自定义类型,在定义的时候已经加了*所以,声明id指针的时候不需要再加*了。
    2). id指针是1个万能指针,任意的OC对象都可以指.
  3. NSObject和id的异同.
    相同点: 万能指针,都可以执行任意的OC对象。
    不同点: 通过NSObject指针去调用对象的方法的时候,编译器会做编译检查,
    通过id类型的指针去调用对象的方法的时候,编译器直接通过,无论调用什么方法。
    注意: id指针只能调用对象的方法,不能使用点语法,如果使用点语法就会直接报编译错误 。如果要声明1个万能指针 千万不要使用NSObject 而是使用id

instancetype --- id

id和instancetype的区别.

  1. instancetype只能作为方法的返回值,不能在别的地方使用。
    id既可以声明指针变量,也可以作为参数,也可以作为返回值。
  2. instancetype是1个有类型的代表当前类的对象。
    id是1个无类型的指针,仅仅是1个地址,没有类型的指针。

new方法

创建对象,我们之前通过new方法
类名 *指针名 = [类名 new];
new实际上是1个类方法,其作用为:

  1. 创建对象。
  2. 初始化对象。
  3. 把对象的地址返回。

new方法的内部,其实是先调用的alloc方法,再调用的init方法。
alloc方法是1个类方法。作用: 那1个类调用这个方法就创建那个类的对象,并把对象返回,分配内存空间。
init方法是1个对象方法。作用: 初始化对象。

init方法
作用: 初始化对象,为对象的属性赋初始值,这个init方法我们叫做构造方法。
init方法做的事情:初始化对象,并为对象的属性赋默认值。
如果属性的类型是基本数据类型就赋值为0,C指针赋值NULL,OC指针赋值nil。
所以,我们创建1个对象如果没有为这个对象的属性赋值这个对象的属性是有默认值的。

重写init方法的规范:
1). 必须要先调用父类的init方法,然后将方法的返回值赋值给self。
2). 调用init方法初始化对象有可能会失败,如果初始化失败,返回的就是nil。
3). 判断父类是否初始化成功,判断self的值是否为nil,如果不为nil说明初始化成功。
4). 如果初始化成功就初始化当前对象的属性。
5). 最后 返回self的值。

自定义构造方法:
1). 自定义构造方法的返回值必须是instancetype。
2). 自定义构造方法的名称必须以initWith开头。
3). 方法的实现和init的要求一样。

文中如果有不对的地方欢迎指出。我是xx_cc,一只长大很久但还没有二够的家伙。

推荐阅读更多精彩内容