$テン*シー*シー-Bootcamp2banner


(1)構造体を使う理由、malloc、freeを使う理由
(2)Objective-C言語を使う理由 その1
(3)Objective-C言語を使う理由 その2

派生から派生する
 サンプル:02-01-folderで、プログラム内容はよりオブジェクト指向らしくなったのですが、派生に関してはもう少し考えておく部分が残っています。
 この点を学ぶためフォルダを派生させたバンドルを考えてみましょう。


バンドル
 バンドルはFinder が特別な扱いをするフォルダのことです。
 Finder はバンドルのアイコンをダブルクリックされても、フォルダのように内包するファイルやフォルダを見せることはしません。
 1つのフォルダをあたかも1つのファイルのようにみせかけるもので、OS X や iOS のアプリケーションの多くが、このバンドルという形式で提供されます。

Control キーを押しながら Finder 上でアプリケーションのアイコンをクリックするとメニューが現れるの で、その中から「パッケージの内容を表示」を選べばアプリケーションバンドルが内包するファイルやフォ ルダを見る事ができます。
1


 これまでの考え方を応用するならバンドルはフォルダを派生させて定義するのが適切という事になります。
 ファイルやフォルダを内包するフォルダの特性を引き継ぎつつ、Item構造体の「コンソール上に名前を表示できる」特性では、ファイルのように自分の名前だけ表示し、内包するファイルやフォルダは表示しないようにします。
 そして、従来のように内包するファイルやフォルダを表示する特性は、あらたに「内容部を表示する:printContents」というバンドルの特性として追加する事にします。

2


 フォルダ用のFolderItem構造体の先頭にItem構造体を埋め込んだように、今度はFolderItem構造体を先頭に埋め込んだBundleItem構造体を用意しましょう。
 「内容部を表示する」特性用には構造体に関数ポインタメンバとしてprintContentsを追加します。

typedef struct BundleItem {
FolderItem super; // FolderItem構造体を先頭に埋め込む
void (*printContents)(struct Item*, int); // 「内容部を表示する」特性用関数
} BundleItem;

// バンドルオブジェクトの作成
Item* BundleItemAlloc();

 あとはバンドルを作成する関数を用意し、BundleItem構造体の関数ポインタメンバを設定すれば派生は完了です。

static void printBundleItem(Item* item, int indent)
{
・・・
printf("[%s]\n", item->name); // バンドルなのでファイル同様、名前を [] で囲んでから表示
}

Item* BundleItemAlloc() ←バンドル作成用関数
{
Item* item = malloc(sizeof(BundleItem));
item->init = initFolderItem;
item->print = printBundleItem; ←printにバンドル専用の表示関数を指定
item->dealloc = deallocFolderItem;
FolderItem* folderItem = (FolderItem*)item;
folderItem->add = addFolderItem;
BundleItem* bundleItem = (BundleItem*)item;
bundleItem->printContents = printFolderItem; ←printContentsにフォルダ用の表示関数を指定
return item;
}

 ただし解決しなければならない問題が1つあります。
 initやdealloc、add、printContentsに設定しているinitFolderItem、deallocFolderItem、addFolderItem、printFolderItemといった関数がstaticとなっているのでFolderItem.c内でしか見えない点です。
 先の記述はFolderItem.c外でおこなうとコンパイル時にXcodeから関数が無いと注意されます。

item->init = initFolderItem; ←FolderItem.cからしか見えない
・・・
item->dealloc = deallocFolderItem; ←〃
・・・
folderItem->add = addFolderItem; ←〃
・・・
bundleItem->printContents = printFolderItem; ←〃

 バンドル用のファイルを用意するのをあきらめて、FolderItem.c内で記述すれば問題は解決しますが、それでは、一番最初に話題にしたプログラムソースの保守管理という点でオブジェクト指向プログラミングを導入した意味が無くなります。
 あくまでBundleItem.cというようにファイルを分離した形でバンドルをプログラミングする方法を考えましょう。

 この場合、手っ取り早い方法はFolderItem.cのinitFolderItem、deallocFolderItem、addFolderItemからstaticを取りはぶくことです。
 注意)static指定のない関数はグローバルスコープになる事(ブートキャンプ(7))を思い出してください。
 関数名をもうすこし利用されにくい名前、例えば記述したプログラマの名前がPNameというなら
 initFolderItem → PName_Folder_initFolderItem

 といった他人が使いそうのない名前にしておき、フォルダから派生させたオブジェクトをプログラミングしようとしているプログラマだけに、その存在を教える(FolderItem.hでは公開しない)…

 それも1つの考えではありますが、ここでは、かなり乱暴ですが次のような方法を取ってみます。

static FolderItem* super = NULL; 関数利用のため、内々で利用するフォルダオブジェクト
・・・
Item* BundleItemAlloc()
{
if (super == NULL)
super = FileItemAlloc();
Item* item = malloc(sizeof(BundleItem));
*((FolderItem*)item) = *super; ←FolderItem構造体に設定された関数ポインタがコピーされる
BundleItem* bundleItem = (BundleItem*)item;
bundleItem->printContents = item->print; ←フォルダのprintはバンドルのprintContentsに移動
item->print = printBundleItem; ←バンドル専用の表示関数を指定
return item;
}

注意)ここでsuperに設定されたフォルダオブジェクトはアプリケーション終了まで存在し続け、アプリケーション終了時にOS側によって解放される事になります。

 これで、バンドルでもprint以外の関数ポインタメンバはフォルダの特性が引き継がれる事になります。
 具体的な作業はサンプル:02-02-folderを用意したので参照してください。

------------
サンプル:02-02-folder.zip

 main関数ではrootをバンドルとして作成しています。
 このようにすると、rootは今までどおりフォルダとしてファイルやフォルダを内包できますが、printでは内容部を表示しません。内容部を表示するにはBundleItem構造体に追加したprintContents関数を使います。

サンプル:02-02-folder main.c

int main(int argc, const char * argv[])
{
// rootバンドル作成
Item* root = BundleItemAlloc();
・・・
// 印刷
root->print(root, 0); ← バンドルなので内包するファイルやフォルダは表示されない

// 内容部印刷
printf("\nShow Contents\n");
((BundleItem*)root)->printContents(root, 0);
・・・

 出力結果は次のようになります。
 rootの表示が[root]となり内包したfolder A、folder B、file Aが表示されません。
 その後にあらためてprintContentsを使ってrootの内容を表示しています。

1.1


 使う側は作成時にバンドルを作る事で、従来部分を変更せずに、ファイル、フォルダ、バンドルに対応できるようになりました。そして必要ならバンドルの機能を使う事もできています。

 このような派生元の機能を派生側で利用する仕組みは、オブジェクト指向プログラミングで重要な意味を持ちます。
 バンドルでは派生元であるフォルダのinitやdeallocをそのまま使っているので、もし、フォルダ側のinitやdeallocに変更があった時も、その変更が正しく反映される事になるからです。
 その点を考えると、FileItem.cやFolderItem.cでおこなっているファイルやフォルダの初期化は適切とはいえません。
 Item構造体部分(ファイルの場合Item構造体をそのまま使っている)をそれぞれが個別に初期化しているからです。

サンプル:02-02-folder FileItem.c

static Item* initFileItem(Item* item, const char* name)
{
strcpy(item->name, name); // nameに指定される名前をコピーする
return item;
}

サンプル:02-02-folder FolderItem.c

static Item* initFolderItem(Item* item, const char* name)
{
strcpy(item->name, name); // nameに指定される名前をコピーする
FolderItem* folderItem = (FolderItem*)item;
・・・
}

 この部分をバンドル同様、派生元の機能を使うようにプログラミングしてみましょう。
 具体的な作業はサンプル:02-03-folderを用意したので参照してください。

------------
サンプル:02-03-folder.zip

 まずはItemAlloc関数を用意し、ファイルでもフォルダでもないItemというオブジェクトを用意できるようにしています。

サンプル:02-03-folder Item.c

static Item* initItem(Item* item, const char* name)
{
strcpy(item->name, name); // nameに指定される名前をコピーする
}
static void printItem(Item* item, int indent)
{
何もしない
}
static void deallocItem(Item* item)
{
free(item);
}
Item* ItemAlloc()
{
Item* item = malloc(sizeof(Item));
item->init = initItem;
item->print = printItem;
item->dealloc = deallocItem;
return item;
}

 そしてバンドルの例にならって、ファイルやフォルダでItemの初期化や破棄機能を利用するようにします。

サンプル:02-03-folder FileItem.c

static Item* file_super = NULL;← static なので BundleItem.c 側の super とぶつかる事はないが
後で説明するデバッガでうまく認識できないので別名にした

・・・
Item* FolderItemAlloc()
{
if (file_super == NULL)
file_super = ItemAlloc();
Item* item = malloc(sizeof(FileItem));
*item = *file_super;
item->printItem = printFileItem;
return item;
}



サンプル:02-03-folder FolderItem.c

static Item* folder_super = NULL;
・・・
static Item* initFolderItem(Item* item, const char* name)
{

folder_super->init(item, name);
FolderItem* folderItem = (FolderItem*)item;
・・・
}
static void deallocFolderItem(Item* item)
{
FolderItem* folderItem = (FolderItem*)item;
if (folderItem->subItems) {
・・・
}

folder_super->dealloc(item);
}

Item* FolderItemAlloc()
{

if (folder_super == NULL)
folder_super = ItemAlloc();
Item* item = malloc(sizeof(FolderItem));
・・・
}



 Runしてもサンプル:02-03-folderと変わりませんが、こちらは例えば派生元のItem構造体のメンバnameの文字列を9バイト固定とせず、可変長に変更した場合に効果が現れます。
 具体的な作業はサンプル:02-04-folderを用意したので参照してください。

------------
サンプル:02-04-folder.zip

 まずはItem構造体のnameをchar型のポインタとします。

サンプル:02-04-folder Item.h

typedef struct Item {
char* name; // 名前を可変長にする。mallocで確保
void (*print)(struct Item*, int);
・・・
} Item;


 この場合Item.cのinitItem関数やdeallocItem関数では、次のようにmallocによる必要な文字バイト分のメモリ確保が必要となります。

サンプル:02-04-folder Item.c

static Item* initItem(Item* item, const char* name)
{
item->name = malloc(strlen(name) + 1);
strcpy(item->name, name);
}
・・・
static void deallocItem(Item* item)
{
free(item->name);
free(item);
}


 しかしファイルやフォルダ、バンドル側に変更は必要ありません。派生によりsuper側の拡張はそのまま反映される事になります。

サンプル:02-04-folder FolderItem.c

static Item* initFolderItem(Item* item, const char* name)
{
super->initItem(item, name); ←変更せずとも可変長対応になっている
FolderItem* folderItem = (FolderItem*)item;
・・・
}

 実際にmain関数で文字列として9バイト以上の文字列を渡していますが、Runしてもどのオブジェクトも問題なく機能しています。

サンプル:02-04-folder main.c

int main(int argc, const char * argv[])
{
・・・
root->init(root, "over 9 bytes name root"); ← 9バイト以上の文字列を指定している
・・・
folderA->init(folderA, "over 9 bytes name folder A"); ← 〃
・・・
folderB->init(folderB, "over 9 bytes name folder B"); ← 〃
・・・
fileA->init(fileA, "over 9 bytes name file A"); ← 〃

 出力結果

[over 9 bytes name root]

Show Contents
over 9 bytes name root
over 9 bytes name folder A
over 9 bytes name folder B
[over 9 bytes name file A]
Program ended with exit code: 0


デバッガを使った確認
 関数群がどのように呼び出されているか興味がある人はデバッガを使って確認してみてください。  例えば、次のように Item.c を選んで、表示されたエディタ画面左の領域(ガターと言います)をクリックすると、マー クが付きます。

a1


 このマークはブレークポイントと言って、Run させるとアプリケーションがその場所で一時停止するようになりま す。マークを付ける作業をブレークポイントを設定すると言います。
 まずは Item.c にある initItem 関数の最初の処理にブレークポイントを設定してみてください。

a2a



 設定したブレークポイントは、ガター外にドラッグアンドドロップする事でいつでも解除できます。

a2




 設定したら実際に Run させてください。次のように initItem 関数の最初の行で一時停止したはずです。
 自動的に Debug Navigator 画面が表示された状態になり、Navigator 画面の下には関数の呼び出し状態が表示され ます。

a3


 表示された Debug Navigator 画面や Debug 画面では、ブレークポイントで一時停止した時点でのアプリケーショ ンの状況を確認できるようになっています。

a4


 そして、この関数の呼び出しリストの中から initFolderItem を選ぶと、initItem 関数が initFolderItem 関数内の、ど こから呼び出されているかもわかるようになっています。

a5


 同じようにして main 関数までたどる事ができます。

a6


 確認できたら Debug 画面の Continue program execution ボタンを押してください。
 これで一時停止が解除され Run が再開されます。

a7


 もっとも、またすぐに次の FolderItem オブジェクトの初期化により同じ場所で止まってしまいますが...

a8


きっちり調べたい人は、このまま folderB まで繰り返してもいいのですし、面倒ならブレークポイントをガター外 にドラッグアンドドロップする事でブレークポイントを解除してから Continue program execution ボタンを押して ください。

Objective C言語へ 
 ここまでの工夫により、オブジェクトは次のような特徴を持つ事になります。

1)派生元の拡張は自動的に派生先オブジェクトに引き継がれる。
2)必要なら派生先オブジェクトで派生元の関数の動きを変更できる。
3)オブジェクトを使う側が同じ指示を与えても、それぞれのオブジェクトに
    見合った結果を得る事ができる。

 そしてこれがオブジェクト指向プログラミングの特徴でもあります。

 このようにオブジェクト指向プログラミングはやろうと思えば C 言語でも実践可能です。
 ただし、本格的に C 言語でオブジェクト指向プログラミングをおこなう場合は、もう少し慎重な設計が必要でしょ う。
 ここで用意したサンプルソースは、C 言語でのオブジェクト指向プログラミングを実体験してもらうために用意さ れた玩具レベルと解釈してください。 玩具レベルでさえ、いちいち構造体の関数ポインタを用意したり設定したりと、さほどスマートに記述できている とは言えません。実用的なプログラミングにしようとするとさらに複雑さが増します。

 派生も今回のような単純なもので済まない場合もあります。
 例えば、最初に紹介した戦闘機、旅客機、装甲車、バスですが、大量輸送、戦闘という分類での派生も考慮すると、装甲車は大量輸送乗り物の派生であり戦闘乗り物の派生でもあるわけです。

3


 このような2重の派生の解決や次回以降に紹介するカプセル化など、C言語でオブジェクト指向プログラミングをおこなおうとすると、拡張や変更時の有効性以上に記述にコストがかかる事になってしまいます。
 そのため、これらをより洗練し容易に記述できるようにC++、Objective Cといった言語が考案されました。
 次回はObjective C言語によるオブジェクト指向プログラミングをみていきましょう。