Objective-C メモリ管理,retain,release 【初級編 第14回】

この記事は2012年11月27日に書かれたものです。内容が古い可能性がありますのでご注意ください。


Objective-C メモリ管理,retain,release 【初級編 第14回】

前回はメモリの管理に関しての説明をしました。
Objective-Cでは、オブジェクトの作成により確保されたメモリが自動で解放されないため、自分で解放する必要があります。
このとき解放するかどうかはretainCountという値により管理され、
allocやreleaseなどのメッセージを送信することでこの値が変更されます。
そして0になると解放されるというものでした。
そのため、メモリの管理の際はこのretainCountに注意しなければなりません。
複数のオブジェクトから必要とされるオブジェクトがあった場合、
どのようにすればよいのかに関して今回は説明しようと思います。

では今まで使用していたコードに付け加えてみましょう。
例えばBMI値のうち最も理想値(22らしいです)に近かった人のデータを保存しておく管理用クラスがあったとします。
Manageクラスとしておきましょう。それ以外は既存のクラスを使用します。

Manage.h

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

@interface Manage:NSObject
{
    Person *person;
}
-(Person*)person;
-(void)setPerson:(Person*)p;
-(void)showBestPerson;
@end

まずManage.hを見てみます。
Person型の変数を使用する予定なのでヘッダーをインポートしておきます。
Manageクラスは理想値に近かった人のデータを保持しておくためのインスタンス変数を持つとします。
そこでPerson型のポインタ変数を用意しています。
次に8行目と9行目にセッターとゲッターを定義しています。
この辺りはその6を見てください。
10行目にある3つ目のメソッドは理想値に近かった人のデータを表示するためのメソッドです。

Manage.m

#import <Foundation/Foundation.h>
#import <math.h>
#import "Manage.h"
#import "Bmi.h"

@implementation Manage
-(Person*)person{
    return person;
}
-(void)setPerson:(Person*)p{
    double num;
    if(person!=nil){
        num = fabs(22 - [Bmi calcBmi:[person weight] height:[person height]]) -
            fabs(22 - [Bmi calcBmi:[p weight] height:[p height]]);
        if( num > 0){
            person=p;
        }
    }else{
        person=p;
    }
}
-(void)showBestPerson
{
    printf("weight:%2.1fn",[person weight]);
    printf("height:%2.1fn",[person height]);
    printf("Bmi:%2.1fn",[Bmi calcBmi:[person weight] height:[person height]]);
}
@end

理想値に近いかどうか出すために今回絶対値を使用するため、「math.h」をインポートしています。
「math.h」は三角関数など算術系の関数が用意されています。
「Manage.h」や「Person.h」など必要なヘッダーはちゃんと読み込む必要があります。
次のところでセッターとゲッターを実装しています。
今回セッターのところでは理想値に近い方のデータを格納する必要があるため、処理を入れています。
登録データがすでにある場合、比較して22からの絶対値が近いほうを登録し、なかった場合はそのまま登録しています。
「showBestPerson」メソッドで保持している理想値に近いユーザのデータを表示するようにしています。

メッセージを送る側のmain.mも少し変えます。

main.m

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

int main(void){
        Person *teacherN = [[Person alloc] initWithWeight:56.0 height:1.72];
        printf("TeacherN BMI:%2.1fn",[Bmi calcBmi:[mine weight] height:[mine height]]);
        printf("%dn",[teacherN retainCount]);

        Manage *manage = [[Manage alloc] init];
        [manage setPerson:teacherN];
        [teacherN release];
        [manage showBestPerson];
        [manage release];

        return 0;
}

7行目でteacherNというPerson型のポインタを作成し、初期化しています。
8、9行目では一応BMI値を表示させ、現在のretainCountを確認しています。
10行目で新しいManage型のポインタ変数を作り初期化したのち、11行目でteacherNを登録しています。
13行目で登録が済んだのでteacherNを解放しています。
14行目では登録されている理想値に近い人のデータを表示しています。
1人しか登録していないのでteacherNの情報がでると思われます。
15行目では管理用につくったクラスを解放しています。

実際の結果は以下のようになります。

セグメンテーション違反が発生しています。14行目の
処理的に見てみるとManageクラス内のインスタンス変数にPersonの情報は格納されているのでデータが出そうですが、
不正な領域にアクセスしているといわれています。
どの部分の処理がまずいかというとまず、セッターですが引数としてポインタ型を受け取っています。
このときポインタ型の引数とint型などの引数の場合で動作が違います。
この辺りはその10を見直してみてください。
データ型の場合は送信してきたセンダーの変数とは別の領域(このメソッド用に確保されたスタック領域ですね)に
そのまま中身の値が格納されます。
しかしポインタ型の場合、渡されるのはあくまでアドレスですのでオブジェクト本体(ヒープ領域にあります)の方が
コピーされるわけではありません。場所情報だけコピーされてるわけです。
main関数で作成されているのでManageオブジェクト自体には所有権がありません。
そのためmainでreleaseされてしまうと本体が消えてしまい、
Manageオブジェクトのインスタンス変数に残るのはすでに解放されてしまった場所のアドレスだけとなってしまいます。
メモリ領域はすでにないためセグメンテーション違反となるわけです。

これを防ぐために表示させてからreleaseするという方法があります。
ただこの場合、複数のPersonデータを登録していくとど、
どのデータが保持されているのか呼び出し元からはわかりにくくなってきます。
判定がManage側で行われるからですね。
かといって最後まで複数のPersonデータを保持し続けるのはメモリの無駄になります。

そこでObjective-CではManage側でその渡されたPersonオブジェクトに対して、
自分が必要としていることを宣言するという方法があります。
前回オブジェクトはretainCountにより必要としているものがどれくらいあるかを管理しているといいました。
この数値を増やせばどこかでreleaseされても0にならないため、ヒープ領域は解放されません。
今回の場合、mainのallocでretainCountが1になっているため、
Manageオブジェクトから+1できればカウントが2となりmainでreleaseされても0にはなりません。
こうしたretainCountを+1したいときに使用するのが「retain」メッセージとなります。
実際Manage.mに付け加えてみましょう。

Manage.m

#import <Foundation/Foundation.h>
#import "Manage.h"
#import "Bmi.h"

@implementation Manage
-(void)setPerson:(Person*)p{
        double num;
        if(person!=nil){
                num = fabs(22 - [Bmi calcBmi:[person weight] height:[person height]]) -
                        fabs(22 - [Bmi calcBmi:[p weight] height:[p height]]);
                if( num > 0){
                        [person release];
                        person=p;
                        [person retain];
                }
        }else{
                person=p;
                [person retain];
        }
}
-(void)showBestPerson
{
        printf("weight:%2.1fn",[person weight]);
        printf("height:%2.1fn",[person height]);
        printf("Bmi:%2.1fn",[Bmi calcBmi:[person weight] height:[person height]]);
}
@end

main.mにもretainCountを確認するための処理を入れてみます。13行目と17行目に表示用の処理をいれてます。

main.m

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

int main(void){
        Person *teacherN = [[Person alloc] initWithWeight:56.0 height:1.72];
        printf("TeacherN BMI:%2.1fn",[Bmi calcBmi:[teacherN weight] height:[teacherN height]]);
        printf("retainCount:%dn",[teacherN retainCount]);

        Manage *manage = [[Manage alloc] init];
        [manage setPerson:teacherN];
        printf("retainCount:%dn",[teacherN retainCount]);
        [teacherN release];
        [manage showBestPerson];
        [manage release];
        printf("retainCount:%dn",[teacherN retainCount]);

        return 0;
}

実行結果は以下のようになりました。


最初のretainCountはPersonオブジェクトを作成した際のカウントです。
allocによって1となっています。
次のretainCountはManageオブジェクトに[setPerson]メッセージを送信した後の値です。
setPersonアクセサメソッド内では受け取ったPersonオブジェクトに対してretainメッセージを送信しています。
それによりretainCountが+1されて2となります。
3つ目のretainCountはManageオブジェクトの解放後の値を表示させています。
これはまずいですね。main.m内でreleaseしているのでPersonオブジェクトのretainCOuntは-1されて1となります。
しかし次のところでManageオブジェクトが解放されているのにretainCountが1のままになっています。

Manageに送られているreleaseメッセージはManageオブジェクトのretainCountを減らすものなので
PersonオブジェクトのretainCountには関係していません。
直後にプログラムが終了しているので今回はいいですが、処理が続く場合ヒープ領域に確保されたままとなります。
そのためManageオブジェクトで受け取ったPersonオブジェクトにretainしたのであれば、
そのオブジェクトに対してきっちりreleaseをしなければなりません。

そこでManage.mで以下の構文を付け加えます。

-(void)dealloc{
        [person release];
        [super dealloc];
}

前回出てきたようにretainCountが0になった際に呼び出されるメソッドがdeallocです。
このメソッドはNSObject内に定義されているのでこれをオーバーライドして使用します。
ManageのretainCountが0になると内部に保持しているPersonオブジェクトを解放する必要があるので
このメソッド内の処理で保持しているPersonオブジェクトに対してreleaseメッセージを送信します。


セグメンテーション違反がでていますが、これは解放されたオブジェクトに対してretainCountメッセージを送信しているからです。
Personオブジェクトは解放されているということですね。
このようにretainやreleaseなどのメッセージを送信して、Objective-Cではメモリ領域を管理する必要があります。

今回のまとめとしては

  • 他のところで作成されたオブジェクトに対して所有する必要がある場合は、retainメッセージを使用してretainCountを+1する必要がある
  • retainメッセージを送った場合、そのObjectの解放処理を入れておかないと作成元でreleaseされても解放されない
  • 上記の状態にならないようにretainCountが0になると呼び出されるdeallocを使用して、所有しているオブジェクトを解放する

といったところでしょうか。

  • このエントリーをはてなブックマークに追加

PAGE TOP