Objective-C对象的申请空间与初始化

对象分配空间与初始化

对象分配空间与初始化

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

  • 为新对象动态分配内存空间
  • 初始化新分配的内存,并赋初值

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

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 的返回值。

Objective-c代码  

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

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

Objective-c代码  

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

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

Objective-c代码  

  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, 用于展示对象是如何创建的:

Objective-c代码  

  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. 例子显示了你在调用父类的预设初始化函数前可以做的事情.

Objective-c代码  

  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. 这些步骤一般来说都是处理在检测父类初始化方法的返回值之后的一大块代码区域中的,这也是实践中的常规模式—  一如之前的例子:

Objective-c代码  

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

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

Objective-c代码  

  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. }

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

Objective-c代码  

  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 发送初始化消息:

Objective-c代码  

  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::

Objective-c代码  

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

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

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

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

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

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

Objective-c代码  

  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::

Objective-c代码  

  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 就有这些方法(当然不是全部):

Objective-c代码  

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

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

Objective-c代码  

  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实例会被创建. 它分配和初始化了一个共享的单例:

Objective-c代码  

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

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

时间: 2024-08-02 00:38:32

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

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 Name zmq_msg_init_size - 使用一个指定的空间大小初始化ZMQ消息对象 Synopsis int zmq_msg_init_size (zmq_msg_t *msg, size_t size); Description zmq_msg_init_size()函数会分配任何被请

C++学习笔记(十五):vector对象在内存空间中是如何增长的

vector对象在内存空间中是如何增长的   我们都知道vector对象是动态存储的,从这一点看有点像链表,可以动态的增加或减少元素.我们也知道链表中是有指针变量,专门用于存储上一个和下一个元素的地址.正是因为这两个指针的存在,我们才能做到动态的存储数据,即不用像数组那样必须事先申请好空间.链表的缺点就是不能够快速的随机访问其中元素,必须通过指针层层查找. 但是,vector既可以实现动态存储数据,而且支持快速随机访问(用下标或者指针访问元素).对于能够用下标查找的数据类型,其存储方式必定是连续

动态申请空间

#include<iostream.h>#include<stdlib.h>void main(){ int *p=(int *)malloc(10); int *cp=(int*)calloc(10,4)://在内存动态存储区分配n个长度为size 的连续空间,函数返回值为指向分配域起始地址的指针 int *yy=(int *)realloc(p,10); cout<<p<<"  "<<sizeof(p)<<en

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

Item4 确保对象被使用前进行初始化 C++在对象的初始化上是变化无常的,例如看下面的例子: Int x; 在一些上下文中,x保证会被初始化成0,在其他一些情况下却不能够保证.看下面的例子: Class Point { Int x,y; }; Point p; P的数据成员有时候保证能够被初始化(成0),有时候却不能.如果你从不存在未初始化对象的语言中转到c++, 就需要注意了,因为这很重要. 使用未初始化对象的坏处 读取未初始化的值会产生未定义的行为.在一些平台中,仅仅读取未初始化的值就会让

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

数组空间的初始化就是为每一个标签地址赋值.按照标签逐一处理.如果我们需要为每一个内存赋值,假如有一个int a[100];我们就需要用下标为100个int类型的空间赋值.这样的工作量是非常大的,我们就想到了让编译器做一些初始化操作,初始化操作是第一次赋值,第二次赋值就不能再这样赋值了. int a[10]=空间: 我需要给它一个空间,让它对这里面的值进行批量处理:比如int a[10]={10,20,30}; //a[1]=10,a[2]=20,a[3]=30,a[4]=-=a[9]=0所以实际

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

无法为数据库 XXX 中的对象XXX 分配空间,因为 'PRIMARY' 文件组已满.请删除不需要的文件.删除文件组中的对象.将其他文件添加到文件组或为文件组中的现有文件启用自动增长,以便增加可用磁盘空间. 原因是装了mssql express 2005 版本,该版本最大的限制是4G 百度说明: 1.数据库的大小限制:SQL Server 2005 Express 和SQL Server 2008 Express 数据库的大小限制最大为 4GB,最新版本的SQL Server 2008 R2 E

条款47: 确保非局部静态对象在使用前被初始化

class FileSystem { ... }; // 这个类在你 // 的程序库中 FileSystem theFileSystem; // 程序库用户 // 和这个对象交互 ////////////////////////////////////////////////////////// class Directory { // 由程序库的用户创建 public: Directory(); ... }; Directory::Directory() { 通过调用theFileSystem

条款04:确定对象使用前已被初始化

目录 1. 总结 2. 构造函数体 VS 初始化列表 3. 对象的初始化顺序问题 1. 总结 无论是在初始化列表中,还是在构造函数体内,请为内置类型对象进行手工初始化,因为C++不保证初始化它们 最好使用初始化列表进行初始化,而不要在构造函数体中使用赋值:初始化列表最好列出所有的成员变量,其排列顺序应该和它们在class中的声明顺序相同 为了避免"不同源文件内定义的non-local static对象在编译时的初始化顺序"问题,请以local static对象替换non-local s

利用Java反射实现JavaBean对象相同属性复制并初始化目标对象为空的属性的BeanUtils

有时遇到将数据传输对象转换成JSON串会将属性值为空的属性去掉,利用Java反射实现JavaBean对象数据传输对象的相同属性复制并初始化数据传输对象属性为空的属性,然后转换成JSON串 package com.banksteel.util; import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.Arrays;import java.ut