$テン*シー*シー-Bootcamp2banner


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


オブジェクト指向プログラミングにおける派生とは
 例えば飛行機は空を飛び、車は地上を走ります。
 もし戦闘機、旅客機、装甲車、バスを、飛行機と車で分類せよといわれれば次のように分類する事になるでしょう。

1


 これは飛行機の特性を「主に空を飛ぶ」とし、車の特性を「主に地上を走る」と考えた分類です。
 さらに飛行機という分類を他の何らかの特性で分類したものが、戦闘機であり旅客機となります。この時の戦闘機、旅客機と飛行機の関係が派生です。
 車についても同じことが言えます。

2


 この派生という人間が日頃から慣れ親しんだ概念が、オブジェクト指向プログラミングにも取り入れられています。

 例えば、前回のファイルやフォルダは「コンソール上に名前を表示できる」特性を持つオブジェクトでした。
 ファイルの場合は名前の周りを[ ]で囲み表示オブジェクトです。
 フォルダの場合はフォルダやファイルを内包でき、それらの名前も一緒に表示するオブジェクトです。

3


 前回のサンプル:01-06-folderでの、Item構造体メンバであるprintやsubItemCountへの設定は、Item構造体のファイルやフォルダへの派生作業であったととらえる事もできます。

4


 ここで重要な点は「コンソール上に名前を表示できる」特性をもつオブジェクトから派生したオブジェクトは「コンソール上に名前を表示できる」特性を引き継ぐという概念です。

5


 前回は派生という概念の説明前だったので

 関数initFileItemを使って初期化するItem構造体だけが、構造体メンバprintに関数printFileが設定されるのでifによる分岐は必要なくなる。

といった表現をしましたが、これをオブジェクト指向プログラミングとして表現するなら

 Item構造体から派生させたオブジェクトはいずれも「コンソール上に名前を表示せよ」という希望を伝えることで、コンソール上で何らかの表示がおこなわれる。
 そのさい、malloc、initItemの組み合わせで作成されるオブジェクトはItem構造体から派生したフォルダとして振る舞い、malloc、initFileItemの組み合わせで作成されるオブジェクトはItem構造体から派生したファイルとして振る舞う。


といった表現になります。

サンプル:01-06-folder main.c

int main(int argc, const char * argv[])
{
↓Item構造体から派生したファイルオブジェクトの作成
Item* root = initItem(malloc(sizeof(Item)), "root"); 
・・・
// 印刷
root->print(root, 0); ←「コンソール上に名前を表示せよ」という希望を伝えている

 乱暴にいうなら

  • オブジェクトの振る舞いはオブジェクト自身が決める

  • 派生オブジェクトは派生元の特性を引き継ぐ


 このような概念(ルール)を守ってプログラミングするスタイルがオブジェクト指向プログラミングだと考えればいいでしょう。
 概念でしかないので、C言語でもオブジェクト指向プログラミングは十分に可能なわけです。
 では、オブジェクト指向プログラミングをおこなうためにC言語を拡張する必要性はどこにあるのでしょうか?
 それにはもう少し複雑な派生関係を考える必要があります。

複雑な派生関係をC言語で表現する
 オブジェクトの構造(C言語のなら構造体定義)や派生関係は、プログラムする者(プログラマ)が自由に設計してかまいません。
 ただし、効率的か、保守しやすいか、という点でプログラマの技量が問われる事になります。
 例えば、前回のItem構造体やその派生の設計はメモリ消費の点では適切とはいえません。
 Item構造体の設計が元々フォルダのみを考えたものだった事から、subItemsというファイルでは使わない構造体メンバを、ファイルオブジェクトを作成した時も抱えてしまっています。

6
オブジェクト指向を進めるとファイルにはsubItemCountも必要なくなるが、それはこの後で


 そのため、ファイルオブジェクトを大量に作成した場合、貴重なメモリ空間に利用されない領域が大量に発生します。

7


 この問題を解決するには、ファイルとフォルダの構造体を別々に定義する次のような設計が考えられます。

8


 具体的な作業はサンプル:02-01-folderを用意したので参照してください。

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

 このサンプルは、ファイルや、フォルダオブジェクトを作って利用するmain.c、Item構造体定義にItem.h、ファイル用にFileItem.h/c、フォルダ用にFolderItem.h/cという構成になっています。

9


 main関数で作成するフォルダやファイルオブジェクトの構成はサンプル:01-06-folderと同じです。
 そのためRunした時のコンソールの出力もサンプル:01-06-folderと変わりません。
 異なるのはプログラムソースだけで、今回はItem構造体のメンバを「コンソール上に名前を表示する」特性に必要なものだけに絞りました。

typedef struct Item {
char name[10]; // 名前
void (*print)(struct Item*, int); // 出力用関数
} Item;

 ファイルオブジェクトの場合、この構造体で十分なので、そのままItem構造体を利用します。
 フォルダオブジェクトは、先頭にItem構造体を埋め込みsubItemCount、subItemsを追加した、新しい構造体FolderItemを定義し利用します。

typedef struct FolderItem {
Item super; // Item構造体を先頭に埋め込む
int subItemCount; // 自分が内包するフォルダやファイルの数
struct Item** subItems; // 自分が内包するフォルダやファイルの配列
} FolderItem;

 フォルダオブジェクトでは、このように定義したFolderItem構造体をmallocで確保し、戻されたメモリ先頭番地をItem構造体のポインタとして使用します。
 FolderItem構造体の先頭番地にはItem構造体を埋め込んでいるので、Item構造体の各メンバへのオフセットはまったく同じとなりItem構造体メンバのprintをそのまま利用できます。

10


 そしてFolderItem構造体として利用したいときは、mallocで戻されたメモリ番地をFolderItem構造体のポインタとして扱えばいいわけです。

11


 ファイルオブジェクトを作る時はItem構造体分のメモリ領域を確保し、フォルダオブジェクトを作る時だけFolderItem構造体分のメモリ領域を確保する事で、大量のファイルオブジェクトを作ってもメモリを無駄にする事なく、コンソールへの出力もできる事になります。

 ただし、この事により当初から利用しているItem構造体を破棄するための関数deallocItemはファイル用、フォルダ用に分ける必要がでてきます。

12


 そのためdeallocItem関数もprint同様、関数ポインタとして構造体メンバに追加しファイル、フォルダ別に用意します。また、初期化に関しても同じ事がいえるので関数ポインタとして構造体メンバに追加しています。
 最終的なItem構造体の定義は次のようしました。

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

typedef struct Item {
char name[10];
void (*print)(struct Item*, int);
struct Item* (*init)(struct Item*, const char*); // 初期化用関数
void (*dealloc)(struct Item*); // 破棄用関数
} Item;


 addItem関数もオブジェクト指向らしくFolderItem構造体に持たせる事にしています。

サンプル:02-01-folder FolderItem.h

typedef struct FolderItem {
Item super;
int subItemCount;
struct Item** subItems;
void (*add)(struct FolderItem*, Item*); // 内包するファイル、フォルダ追加用関数
} FolderItem;


 このように初期化、破棄もprint同様オブジェクト自身に持たせる事で、結果的にItem構造体の大きさが前回の大きさに戻っていますが、これは前回のオブジェクト指向プログラミングが限定的なものだったからです。
 今回のように初期化、破棄もオブジェクトに任せるのが

オブジェクトの振る舞いはオブジェクト自身が決める

というオブジェクト指向プログラミングの概念に則ったものといえます。あくまで

ファイルオブジェクトの大きさ < フォルダオブジェクトの大きさ

という点でメモリ効率を考えているとして読み進んでください。


 実際にはメモリ効率を考え、次のような関数ポインタだけを集めた構造体をファイル用、フォルダ用にそれぞれ1つ用意し、Item構造体やFolderItem構造体には、その構造体のポインタを用意するといった工夫をします。
 また、今回のように先頭に構造体を埋め込むようにすると、将来埋め込まれた構造体側の構成が変更すると埋め込んだ側の構造体を使うプログラムソースの再コンパイルが必要となるので、構造体の構成が変化しないよう、例えばItem構造体ならnameといった変数もメンバとして直接配置させず別の構造体として用意し、Item構造体malloc時に同時にmallocしてItem構造体に設定するよう工夫します。

struct Item;

typedef struct ItemFuncs {
void (*print)(struct Item*, int); // 出力用関数
struct Item* (*init)(struct Item*, const char*); // 初期化用関数
void (*dealloc)(struct Item*); // 破棄用関数
} ItemFuncs; ←この構造体をstatic変数として1つだけ用意する

typedef struct ItemAttributes {
char name[10]; // 名前
←ここに将来メンバを追加してもItem側の構成は変化しないで済む
} ItemAttributes; ←Item構造体ごとにmallocで確保して管理する

typedef struct Item {
ItemAttributes* attributes; ←Item構造体ごとにmallocで確保して管理したItemAttributesの先頭番地を設定する
ItemFuncs* funcs; ←static変数として用意しているItemFuncs構造体の先頭番地を設定する
} Item; ←item構造体のメンバは2つの構造体のポインタで固定され変化させない

 そこまで細かい管理をやってしまうと、どんどん説明が長くなってしまうのでここでは省略しています。

 ファイルや、フォルダのオブジェクト用にメモリ領域をmallocで確保し各関数を割り当てる作業は、それぞれの作成用関数としてまとめました。

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

Item* FileItemAlloc()
{
Item* item = malloc(sizeof(Item));
item->init = initFileItem;
item->print = printFileItem;
item->dealloc = deallocFileItem;
return item;
}

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

Item* FolderItemAlloc()
{
Item* item = malloc(sizeof(FolderItem));
item->init = initFolderItem;
item->print = printFolderItem;
item->dealloc = deallocFolderItem;
FolderItem* folderItem = (FolderItem*)item;
folderItem->add = addFolderItem;
return item;
}

 main関数ではこれらFileItemAlloc、FolderItemAllocを使いファイルやフォルダオブジェクトを作成して利用します。
 FolderItemAllocは戻り値をItem構造体のポインタとしたので、そのままItem*型の変数rootに記憶させて利用しています。
 これでrootはFileItemAllocで作成したオブジェクトなのでinitやprint、deallocはフォルダとして振る舞います。

Item* root = FolderItemAlloc();
root->init(root, "root"); ←フォルダとしての初期化

 そしてFileItemAllocで作成したオブジェクトのinitやprint、deallocはファイルとして振る舞います。

Item* fileA = FileItemAlloc();
fileA->init(fileA, "file A"); ←ファイルとしての初期化

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

・・・
#include "FileItem.h"
#include "FolderItem.h"

int main(int argc, const char * argv[])
{
// rootフォルダ作成、初期化
Item* root = FolderItemAlloc();
root->init(root, "root");
・・・
// fileAファイル作成、初期化
Item* fileA = FileItemAlloc();
fileA->init(fileA, "file A");
・・・
// rootフォルダにfileAを内包させる
((FolderItem*)root)->add((FolderItem*)root, fileA);
// 印刷
root->print(root, 0);
// 破棄
root->dealloc(root);
return 0;
}

 サンプル:02-01-folderをRunすると前回のサンプル:01-06-folderと同じ結果になるはずです。
 動作結果は同じですが、プログラム内容はよりオブジェクト指向らしいスタイルとなっています。

 ただ、派生に関してはもう少し考えておく部分が残っています。
 次回はその点について考えるために、フォルダを派生させたバンドルを用意してみましょう。