对象分配空间与初始化

对象分配空间与初始化

使用Objective-C语言创建一个对象有两个步骤,你必须:

  • 为新对象动态分配内存空间

  • 初始化新分配的内存,并赋初值

不经过如上两步,一个对象就没有完全功能化。每个步骤都可以分步完成,不过一般的都是在用写在同一行的代码实现:

  1. id anObject = [[Rectangle alloc] init];

把分配空间和初始化分离,你就可以分开的操作这两步,那么对其的修改也是隔离的。下文将首先关注分配内存空间,而后是初始化,接着讨论它们是如何控制和修改的。

在Objective-C中, 新对象的内存申请的类方法是定义在NSObject类中的,NSObject定义了两个主要方法:allocallocWithZone.

这两个方法会分配足够的内存以容纳全部的实体变量,不需要在子类中重写.

allocallocWithZone:方法初始化新分配的对象的isa实体变量,让它可以指向对象的类(类对象).其他的实体变量都会被设置为0.通常,一个对象需要在使用前做更针对的初始化.

初始化是不同的类的实体方法的责任, 为了方便,一般都缩写为"init".如果方法不需要参数,那么初始化方法名就用这四个字母足矣,如果需要参数,就写成以"init"为前缀的参数标签。比如,NSView对象可以用initWithFrame:方法初始化.

每个声明了实体变量的类必须提供init...的方法初始化这些实体变量.NSObject类声明了isa变量,并定义了init方法.然而,因为isa是当对象的内存分配后就已经初始化完成的,所有的NSObjectinit方法仅仅是返回self.NSObject声明这个方法主要是为了建立之前所描述的命名习惯.

返回的对象

init...方法通常用于init方法的承接着初始化实体变量,并返回该承接者。返回对象供无错的使用正是其责任。

不过,在某些情况,这个责任可能意味着返回和承接者不同的对象。比如一个类保持了一些有名字的对象,它就可能提供一个叫做initWithName: 的方法去初始化新对象.如果不是每个对象都有各自的名字的话,那么initWithName: 可能会拒绝将同一个名字付给两个对象。当我们想要对一个新对象赋名字时,它发现这个对象的名字已经有对象使用过了,那么它可能会将这个新对象释放,并返回已经使用这个名字的老对象,这样可以确保我们想要构建的对象在同一个名字的前提下将是同一个对象。

在另一些的情况,可能无法让init... 方法做到它本来应该完成的任务。比如,一个叫initFromFile: 的方法设计上是想让其获得参数的文件的数据,但如果参数里的文件并不实际存在,这必然无法做到初始化。这种情况下,init... 方法将会 释放承接者并返回nil, 表明被请求的对象无法被创建。

综上 init... 方法并不一定返回承接者即刚刚分配空间的对象甚至可能返回nil, 所以初始化方法的返回值是相当重要的,它未必返回的就是alloc 或 allocWithZone:创造的对象.下面的实例代码是非常危险的,因为忽略了init 的返回值。

  1. id anObject = [SomeClass alloc];
  2. [anObject init];
  3. [anObject someOtherMessage];

取而代之,为了安全的初始化对象,你应该始终将发送分配空间和初始化消息写在一行代码中

  1. id anObject = [[SomeClass alloc] init];
  2. [anObject someOtherMessage];

如果init... 方法有返回nil的可能 (见 “Handling Initialization Failure”),你应该在继续处理之前校验返回值:

  1. id anObject = [[SomeClass alloc] init];
  2. if ( anObject )
  3. [anObject someOtherMessage];
  4. else
  5. ...

实现一个初始化方法

当一个新对象被创建后,它所占用的内存的每一bit(除了isa 外)都被置为0,因此所有实体变量的初值也是0. 在某些情况,这样就可以满足你对该对象的初始化的要求,但别的情况中,你要为实体变量提供别的默认初值,或者你可以给初始化方法传参并利用参数初始化,那么你就需要写一个自定义的初始化方法。在Objective-C中,自定义初始化方法要遵守比其他方法更多的限制与惯例。

限制与惯例

这里时一些仅适用于初始化方法的限制与惯例:

  • 惯例上,初始化方法的名字由init开始。比如Foundation framework里就包括initWithFormat:initWithObjects:和 initWithObjectsAndKeys: 

  • 初始化方法的返回类型应该是id.

    返回类型规定为id是因为id类型表明该类是故意不写明,从而不类型绑定并易于修改,具体类型将依赖于调用时的上下文。比如NSString提供了initWithFormat:的方法,当参数是一个NSMutableString (一个NSString的子类)时, 方法将返回一个NSMutableString, 而不是NSString(不好意思,我没试验出来这种情况). (也可以看这里的单例示例“Combining Allocation and Initialization.”)

  • 在自定义初始化方法的实现中,你必须调用预设的初始化方法(designated initializer).

    预设的初始化方法在 “The Designated Initializer”里有描述; 而关于这个问题的完全解释在 “Coordinating Classes.”

    简而言之,如果你正在实现一个新的预设初始化方法,它必须要调用父类的预设初始化方法. 如果你要实现别的初始化方法,它就必须调用本类的预设初始化方法,或者再别的初始化方法间接调用到了预设初始化方法。默认的预设初始化方法(如 NSObject), 就是init.

  • 你应该将self 用初始化方法的返回值赋值,因为初始化方法可能返回的是别的对象而非原先的self.

  • 如果你要在初始化方法里对实体变量赋值,应该采用直接赋值而非访存方法。

    直接赋值避免了访存方法可能触发的副效应.

  • 在初始化方法的接触,你必须返回self 除非初始化失败,那时你可以返回nil.

    初始化方法失败在 “Handling Initialization Failure.”有更详细的讨论

下面的示例描述如何实现一个继承自NSObject 的类的自定义初始化方法,该类含有一个实体变量creationDate, 用于展示对象是如何创建的:

  1. - (id)init {
  2. // Assign self to value returned by super's designated initializer
  3. // Designated initializer for NSObject is init
  4. self = [super init];
  5. if (self) {
  6. creationDate = [[NSDate alloc] init];
  7. }
  8. return self;
  9. }

(关于使用 if (self) 的模式在“Handling Initialization Failure.” 有讨论)

初始化方法并不需要为每个实体变量提供参数. 比如一个类需要它的实例一个名字和一个数据源,它可能会提供一个形如initWithName:fromURL: 的方法,但非必须的实体变量可能仅需要一个任意值或默认的空值. 那么设置这些实体变量依赖于类似 setEnabled:setFriend:,和 setDimensions: 这样的方法在初始化完成后修改默认值.

下面的例子展示了使用单个参数的初始化方法. 在本例中,类继承自NSView. 例子显示了你在调用父类的预设初始化函数前可以做的事情.

  1. - (id)initWithImage:(NSImage *)anImage {
  2. // Find the size for the new instance from the image
  3. NSSize size = anImage.size;
  4. NSRect frame = NSMakeRect(0.0, 0.0, size.width, size.height);
  5. // Assign self to value returned by super's designated initializer
  6. // Designated initializer for NSView is initWithFrame:
  7. self = [super initWithFrame:frame];
  8. if (self) {
  9. image = [anImage retain];
  10. }
  11. return self;
  12. }

该例子并不是展示如何应对初始化时发生的问题,如何处理这种问题将在下一段讨论.

处理初始化失败

一般来说,如果初始化方法里产生了问题,你应该对self 调用 release 并返回 nil.

下面时两大理由:

  • 任何对象 (无论时你的类或子类或外部调用者) 可以在初始化方法里接受到nil 并处理. 在不太可能的情况下,调用者会对对象在调用前建立很多外部关联,你必须要取消这些关联.

  • 你必须确保dealloc 方法在被部分初始化的对象上的安全调用.

注意: 你应该仅在失败时对self 调用release. 如果你在调用父类的初始化函数就返回了nil 就不应该调用release. 你也应该仅清理已经建立的关联,而不是在dealloc 里处理并返回nil. 这些步骤一般来说都是处理在检测父类初始化方法的返回值之后的一大块代码区域中的,这也是实践中的常规模式—  一如之前的例子:

  1. - (id)init {
  2. self = [super init];
  3. if (self) {
  4. creationDate = [[NSDate alloc] init];
  5. }
  6. return self;
  7. }

而下例是出自 “限制与管理” ,展示如何处理参数为不合适的值:

  1. - (id)initWithImage:(NSImage *)anImage {
  2. if (anImage == nil) {
  3. [self release];
  4. return nil;
  5. }
  6. // Find the size for the new instance from the image
  7. NSSize size = anImage.size;
  8. NSRect frame = NSMakeRect(0.0, 0.0, size.width, size.height);
  9. // Assign self to value returned by super's designated initializer
  10. // Designated initializer for NSView is initWithFrame:
  11. self = [super initWithFrame:frame];
  12. if (self) {
  13. image = [anImage retain];
  14. }
  15. return self;
  16. }

再下例展示了最好做法,当有问题的时候,还会返回错误信息:

  1. - (id)initWithURL:(NSURL *)aURL error:(NSError **)errorPtr {
  2. self = [super init];
  3. if (self) {
  4. NSData *data = [[NSData alloc] initWithContentsOfURL:aURL
  5. options:NSUncachedRead error:errorPtr];
  6. if (data == nil) {
  7. // In this case the error object is created in the NSData initializer
  8. [self release];
  9. return nil;
  10. }
  11. // implementation continues...

不要使用异常去反馈此类错误,更多信息可查阅 Error Handling Programming Guide.

协助类

一个类的init... 方法一般值用于初始化本类中声明的实体变量. 通过继承获得的变量则是向super 发送初始化消息:

  1. - (id)initWithName:(NSString *)string {
  2. self = [super init];
  3. if (self) {
  4. name = [string copy];
  5. }
  6. return self;
  7. }

super 发送的初始化消息将会让继承层次上所有父类连锁初始化. 因为这是最先调用的,所以可以确保父类的实体变量将在子类的实体变量之前初始化。比如一个Rectangle对象必然依次初始化为一个NSObject对象,一个Graphic对象,一个Shape对象.

initWithName: 方法与继承的init 方法关联如下图所示.

Figure   结合继承的初始化方法

一个类必须保证所有继承的初始化方法都可用。比如类A定义了init 方法,而它的子类B定义了initWithName:方法, 就如上图所示。那么B必须确保init 消息仍然可以成功的初始化一个B的实体. 最简单的方式就是覆盖继承而来的init 方法然后调用initWithName::

  1. - init {
  2. return [self initWithName:"default"];
  3. }

如此,initWithName: 方法将会依次调用到继承的方法,如之前所述。下图则是描述了B类的init调用顺序.

在你定义的类中,覆盖继承而来的初始化方法,将使得你的代码更加容易移植到别的应用中去。如果你遗漏了一个继承的方法没有覆盖,别人可能会错误的初始化一个你的类的实例.

预设(默认)的初始化方法

在协作类里的例子里initWithName: 应该做为该类的预设(默认)初始化方法。预设初始化方法就是每个类中确保继承来的变量都可以被初始化的方法(通过向父类发信息调用继承方法). 它也是本类别的初始化方法需要在内部调用的方法. 按照Cocoa的惯例,预设初始化方法永远都是最自主的决定新实例的所有特性的方法(一般来说就是参数最多的方法,但不一定).

定义子类时,了解预设初始化方法是很重要的。比如类C,它是类B的子类,实现了一个initWithName:fromFile: 的方法,但除此之外,你还必须确保继承而来的initinitWithName: 方法对C类仍然可用, 当然你可以简单的直接在initWithName: 方法中调用initWithName:fromFile:.

  1. - initWithName:(char *)string {
  2. return [self initWithName:string fromFile:NULL];
  3. }

对于C类的实体,继承而来的init方法不需要覆盖就自然是调用initWithName:的新版本,即在内部调用initWithName:fromFile:.方法调用的关系如下图

上图其实忽略了一个重要的细节,即initWithName:fromFile: 方法,也就是C类的预设初始化方法, 需要向父类发送消息调用继承而来的初始化方法,但究竟调用哪个方法,是init还是initWithName:? 结论是不能调用init, 有两个理由:

  • 会引发循环调用(init调用C类的initWithName:, 然后initWithName:又会调用initWithName:fromFile:, 而该方法又会再次调用init,如此循环).

  • 这样就不能复用B类的initWithName:方法了.

因此, initWithName:fromFile:必须调用initWithName::

  1. - initWithName:(char *)string fromFile:(char *)pathname {
  2. self = [super initWithName:string];
  3. if (self) {
  4. ...
  5. }

一般原则: 预设初始化方法在其内部调用父类的预设初始化方法。

预设初始化方法会连锁的向各自的父类的预设初始化方法发送消息, 而其他的初始化方法则向本类的预设初始化方法发消息.

下图展示了AB, 及C类的初始化方法的关联. 发向self的消息画在左侧,发向父类的画在右侧.

注意在B类的init是向self发消息调用initWithName:方法的. 因此当实际类型是B的时候,init方法就是调用B类的initWithName:方法, 而当实际类型是C类时,则调用C类的版本.

结合空间分配和初始化

在Cocoa中,一些类定义了将分配空间和初始化这两步结合在一起的创建方法,返回新的初始化完毕对象。这些方法经常被称坐便捷构造方法,并拥有 className... 的形式, className 就是该类的名字. 比如, NSString 就有这些方法(当然不是全部):

  1. + (id)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc;
  2. + (id)stringWithFormat:(NSString *)format, ...;

类似的, NSArray也定义了如下便捷方法:

  1. + (id)array;
  2. + (id)arrayWithObject:(id)anObject;
  3. + (id)arrayWithObjects:(id)firstObj, ...;

重要: 如果没有垃圾回收机制时,在使用这些方法的时候必须理解其内存管理机制(见 “Memory Management”). 你必须阅读Memory Management Programming Guide 去理解这些快捷构造方法的策略。

快捷构造方法的返回类型都是id,原因见“Constraints and Conventions.”中的讨论。

如果初始化方法必须要通知某些信息给空间分配,那么将空间分配和初始化结合在一起就显得相当有用. 比如,假设初始化方法需要的数据来自一个文件,且该文件含有足够的数据去初始化不止一个对象,那么不打开该文件,是不可能知道到底分配了多少对象空间。此种情况下,你可能会实现一个形如listFromFile: 的方法,方法的参数是文件名. 该方法可能去打开文件,看看到底有多少对象被分配了空间,再创建一个足够大的列表对象,其中包含了所有的新对象。过程就是从文件中读取数据,分配空间并初始化对象集合,将对象集合放入列表,在最后返回列表。

把分配空间和初始化放入单个函数里,对想避免分配不使用的对象也很有用. 正如在 “The Returned Object,” 提到的一样init... 方法某些时候可能会把原对象用别的对象所取代.比如, 当initWithName: 方法传递的name已经使用过了,它可能会释放这个方法的消息接受者对象,并返回之前用这个名字分配好的对象. 这意味着,一个对象可能被分配空间后,不经过使用就立刻被释放.

如果决定消息接受者是否需要初始化的代码写在分配空间的代码里,而不是在init...中, 你就可以避免了对不会使用的实体分配空间的一步.

在下面的例子里,soloist方法确保了不会有超过一个Soloist实例会被创建. 它分配和初始化了一个共享的单例:

  1. + (Soloist *)soloist {
  2. static Soloist *instance = nil;
  3. if ( instance == nil ) {
  4. instance = [[self alloc] init];
  5. }
  6. return instance;
  7. }

注意在此种情况下返回的类型是Soloist *. 因为这个方法返回的是共享的单例实体,强类型是很合适的,这个方法本身就就不应该被重写.

Objective-C对象的申请空间与初始化的更多相关文章

  1. ZeroMQ接口函数之 :zmq_msg_init_size - 使用一个指定的空间大小初始化ZMQ消息对象

    ZeroMQ 官方地址 :http://api.zeromq.org/4-1:zmq_msg_init_size zmq_msg_init_size(3) ØMQ Manual - ØMQ/3.2.5 ...

  2. Java基础-对象的内存分配与初始化(一定要明白的干货)

    首先,什么是类的加载?类的加载由类加载器执行.该步骤将查找字节码(classpath指定目录),并从这些字节码中创建一个Class对象.Java虚拟机为每种类型管理一个独一无二的Class对象.也就是 ...

  3. 008 PCI设备BAR空间的初始化

    一.PCI设备BAR空间的初始化 在PCI Agent设备进行数据传送之前,系统软件需要初始化PCI Agent设备的BAR0~5寄存器和PCI桥的Base.Limit寄存器.系统软件使用DFS算法对 ...

  4. C语言数组空间的初始化详解

    数组空间的初始化就是为每一个标签地址赋值.按照标签逐一处理.如果我们需要为每一个内存赋值,假如有一个int a[100];我们就需要用下标为100个int类型的空间赋值.这样的工作量是非常大的,我们就 ...

  5. 无法为数据库 XXX 中的对象XXX 分配空间,因为 'PRIMARY' 文件组已满。请删除不需要的文件、删除文件组中的对象、将其他文件添加到文件组或为文件组中的现有文件启用自动增长,以便增加可用磁盘空间。

    无法为数据库 XXX 中的对象XXX 分配空间,因为 'PRIMARY' 文件组已满.请删除不需要的文件.删除文件组中的对象.将其他文件添加到文件组或为文件组中的现有文件启用自动增长,以便增加可用磁盘 ...

  6. 读书笔记 effective c++ Item4 确保对象被使用前进行初始化

    Item4 确保对象被使用前进行初始化 C++在对象的初始化上是变化无常的,例如看下面的例子: Int x; 在一些上下文中,x保证会被初始化成0,在其他一些情况下却不能够保证.看下面的例子: Cla ...

  7. python基础——类名称空间与对象(实例)名称空间

    python基础--类名称空间与对象(实例)名称空间 1 类名称空间 创建一个类就会创建一个类的名称空间,用来存储类中定义的所有名字,这些名字称为类的属性 而类的良好总属性:数据属性和函数属性 其中类 ...

  8. day 23 对象的名称空间 类,对象属性和方法 封装 接口提供

    一.对象的特有名称空间 # 对象独有的名称空间:在产生对象时就赋初值 '''class ted: def func(): 当func里不存在参数时,调用时不需要给值 print('hah')ted.f ...

  9. Python 入门基础10 --函数基础3 函数对象、名称空间、装饰器

    今日内容 1.函数对象 2.名称空间与作用域 3.函数的嵌套调用与闭包 4.装饰器 一.函数对象 1.1 定义 函数名存放的就是函数地址,所以函数名也就是对象,称之为函数对象 1.2 函数对象的应用 ...

随机推荐

  1. BEC listen and translation exercise 38

    很高兴看到有这么多人想了解我们的体育设施.It's good to see that there are so many people wanting to find out about our sp ...

  2. linux命令学习笔记(6):rmdir 命令

    今天学习一下linux中命令: rmdir命令.rmdir是常用的命令,该命令的功能是删除空目录,一个目录 被删除之前必须是空的.(注意,rm - r dir命令可代替rmdir,但是有很大危险性.) ...

  3. bzoj 4044: Virus synthesis 回文自动机

    题目大意: 你要用ATGC四个字母用两种操作拼出给定的串: 将其中一个字符放在已有串开头或者结尾 将已有串复制,然后reverse,再接在已有串的头部或者尾部 一开始已有串为空.求最少操作次数. le ...

  4. 【VisualStudio】软件安装中出现的问题

    针对2017版本安装 1. 安装windows通用平台工具出错 报错信息:15605 FQ安装. 2.  LINK : fatal error LNK1104: 无法打开文件“gdi32.lib” 在 ...

  5. redis的五种数据类型及应用场景

    前言 redis是用键值对的形式来保存数据,键类型只能是String,但是值类型可以有String.List.Hash.Set.Sorted Set五种,来满足不同场景的特定需求. 本博客中的示例不是 ...

  6. DevExpress源码编译总结

    独家提供完整可编译sln文件,本篇文章内容包括基础知识(GAC.程序集强签名.友元程序集).编译过程.注册GAC.添加工具箱.多语言支持.运行时和设计时调试 源码地址  链接:http://pan.b ...

  7. NetScaler VPX在Azure上的部署(一)

    本文将介绍NetScaler的VPX部署在Azure China上.包括如何通过vhd文件上传.创建虚拟机,以及如何部署VPX. 一.首先将VHD文件解压,放到目录D:\Azure中.VHD文件的获得 ...

  8. Project Web Server PSI 接口一些常用操作

    对Project Web Server进行二次开发,每天都把自己折腾到12点以后才休息,到处都是坑,研究那些烦人的PSI,国内根本查不到PSI相关的资料,对照API文档一点点谷歌资料,全部英文资料,开 ...

  9. Vue之vue.js声明式渲染

    Html: <div id="app"> {{ message }} </div> Vue: var app = new Vue({ el: '#app', ...

  10. ssh功能模块——paramiko

    参考官网文档:http://docs.paramiko.org/