$テン*シー*シー-Bootcamp2banner


(1)構造体を使う理由、malloc、freeを使う理由


Objective-C言語を使う理由
 アプリケーション実行時においてmalloc、freeを使う・使わないは、アプリケーションの性能に大きな違いをもたらします。
 これに対し構造体を使う・使わないはアプリケーションの性能に、さほどの影響はおよぼさない事も理解しておいてください。やろうと思えば、先のプログラムを構造体を使わずに作成することもできます。
 ただし、それはとても読みにくいC言語ソースとなるでしょう。
 構造体を使う1番の理由は、ソースの可読性をあげることによるプログラミングの生産性や保守効率の向上でしょう。そしてObjective-C言語を使う理由もプログラミングの生産性や保守効率の向上にあります。
 
 ではObjective-C言語を使う事で、どのように生産性や保守効率があがるのでしょうか?
 その事を理解するには、まずオブジェクト指向プログラミングがどういったものなのかを知る必要があります。
 なぜならObjective-C言語は、より効率的にオブジェクト指向プログラミングができるようにするためC言語を拡張したものだからです。

オブジェクト指向プログラミングとは
 オブジェクトを指向したプログラミングとはどういったものでしょう。
 例えばC言語が設計された時代のプログラミングは、関連するデータ群をひとまとめにして構造体とし、この構造体を処理する関数を用意するといったスタイル(style:やり方、流儀、様式)が主流でした。
 仮にこのスタイルを非オブジェクト指向プログラミングと呼ぶ事にしましょう。
 前回作成したアプリケーションが、この非オブジェクト指向プログラミングです。

テン*シー*シー-1

 これに対し、オブジェクト指向プログラミングでは、関連するデータだけでなく、そのデータ群を処理する関数までひとまとめにして構造体とします。

テン*シー*シー-2

 実際に、先のプログラムを拡張し、このオブジェクト指向プログラミングを体験してみましょう。
 記述が複雑になり冗長になりますが、C言語でもオブジェクト指向プログラミングは可能です。

C言語を使ったオブジェクト指向プログラミング
 例えば先のプログラムで、ファイルとフォルダとの違いを示すために、コンソールに名前を表示する時にファイルの場合だけ[]で囲むにはどうすればいいでしょうか?

root
 folder A
 folder B
 [file A] ← rootに追加したファイル"file A"

 その場合、非オブジェクト指向プログラミングでは関数printItemを次のように変更するのが一般的です。

static void printItem(Item* item, int indent)
{
for (int i = 0; i < indent; i++) printf(" ");

if (item->subItemCount == ItemIsFile)
printf("[%s]\n", item->name); // ファイルなので名前を [] で囲んでから表示
else
printf("%s\n", item->name); // フォルダなのでそのまま名前を表示



}


 この変更は、前回定義したItem構造体メンバsubItemCountの、値に対する取り決めに基づいたものです。

static const int ItemIsFile = -1; // Itemがファイルの場合、subItemCountにこの値を設定

typedef struct Item {
char name[10]; // 名前記憶用
int subItemCount; // 自分が内包するフォルダやファイルの数。-1だとファイルを意味する
struct Item** subItems; // 自分が内包するフォルダやファイルの配列へのポインタ
} Item;


 ファイル用に作成するItem構造体は、構造体のsubItemCountメンバを-1に初期化する必要があります。
 そのため専用の関数initFileItemを用意しました。initItemとの違いはsubItemCountをItemIsFile(-1)にする部分だけです。

static Item* initFileItem(Item* item, const char* name)
{
item->subItemCount = ItemIsFile; // 自分がファイルであることを示す
strcpy(item->name, name); // nameに指定される名前をコピーする
item->subItems = NULL; // ファイルなので、自分が内包するフォルダやファイルの配列は無い
return item;
}

 これで、次のようにファイル用のItem構造体fileAを用意しrootに追加する事ができます。

int main(int argc, const char * argv[])
{
Item* root = initItem(malloc(sizeof(Item)), "root");
Item* folderA = initItem(malloc(sizeof(Item)), "folder A");
Item* folderB = initItem(malloc(sizeof(Item)), "folder B");
addItem(root, folderA);
addItem(root, folderB);
Item* fileA = initFileItem(malloc(sizeof(Item)), "file A");
addItem(root, fileA);


 これが非オブジェクト指向プログラミングでの前回サンプル:01-01-folderの機能拡張です。
 サンプルプロジェクトを用意したので、そちらで確認してください。

サンプル:01-02-folder

 main.cを選び、Show the version editorを選択すると、サンプル:01-01-folderからの変更部分が確認できます。

$テン*シー*シー-3

 Runさせるとコンソールには次のように出力されます。

root
 folder A
 folder B
 [file A]

 関数printItemの変更、および関数initFileItemの追加により、Item構造体にファイルという表現が加わりました。

非オブジェクト指向プログラミングでの拡張の問題点
 ところで、このprintItemに対しておこなった変更は、大規模開発時においては、しばしば不可能となります。
 大規模なアプリケーション開発では、printItemやinitItemといった関数がライブラリの形で提供される場合があるからです。
 具体例としてItem構造体関係の関数がライブラリとして提供されたサンプル:01-03-folderを用意したので、そちらで確認してください。

サンプル:01-03-folder

$テン*シー*シー-4
関数printItemはlibitem.aというライブラリファイルとして提供され変更ができない

 サンプル:01-03-folderはRunさせるとサンプル:01-01-folderと同じ動作をしますが、これを拡張しファイル名表示に [ ]を付けさせようとした場合、関数printItemを修正する方法がありません。

 この場合、次のサンプル:01-04-folderように、printItemとは別の関数を用意し、そちらを使うようにしなければいけません。

サンプル:01-04-folder

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

static Item* initFileItem(Item* item, const char* name)
{
サンプル:01-02-folderと同じ処理
}

static void printFileAndFolder(Item* item, int indent)
{
サンプル:01-02-folderのprintItemとほぼ同じだが、再帰呼び出しはprintFileAndFolderになる。



printFileAndFolder(item->subItems[i], indent); // printFileAndFolderの再帰処理



}

int main(int argc, const char * argv[])
{
サンプル:01-02-folderのmainとほぼ同じだが、printItem呼び出しはprintFileAndFolderになる。




printFileAndFolder(root, 0); ←printItemではなくprintFileAndFolderを利用する



}

 今回のサンプル:01-04-folderは単純にmainから1度printItemを呼び出すだけですが、実際の実用的なアプリケーションが、こんな単純なわけはありません。いろいろなところで使われているprintItemはすべてprintFileAndFolderに変更する必要が出てきます。

$テン*シー*シー-5

 利用する関数の変更が、いろいろな部分の変更を引き起こすわけです。

オブジェクト指向プログラミングでの解決策
 この問題点の解決策の1つとして、関連するデータ群だけでなく、そのデータ群を処理する関数までひとまとめにして構造体にするという、オブジェクト指向プログラミングが利用できます。
 実際にサンプル:01-01-folderを、Item構造体と関数printItemをオブジェクト指向プログラミングで作成しなおしたサンプル:folder-01-05を用意しました。
 Show the version editorを使って(サンプル:01-02-folderのところで紹介)、サンプル:01-01-folderとの違いを眺めながら説明を読んでいってください。

サンプル:01-05-folder

 まず、Item構造体に関数printItemをまとめ込むために関数ポインタを使います。メンバ名はprintとしました。

関数ポインタ 参照→

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

typedef struct Item {
char name[10]; // 名前
int subItemCount; // 自分が内包するフォルダやファイルの数。-1だとファイルを意味する
struct Item** subItems; // 自分が内包するフォルダやファイルの配列
void (*print)(struct Item*, int); // 出力用関数
} Item;

Item* initItem(Item* item, const char* name);
void deallocItem(Item* item);
void addItem(Item* item, Item* subItem);
void printItem(Item* item, int indent);


 そして関数initItemでは、構造体メンバのprintに利用したい関数の番地を設定します。

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

Item* initItem(Item* item, const char* name)
{
item->subItemCount = 0; // 自分が内包するフォルダやファイルの数を0にする
strcpy(item->name, name); // nameに指定される名前をコピーする
item->subItems = NULL; // 自分が内包するフォルダやファイルの配列は初期状態では持たない

item->print = printItem; // 出力用に利用する関数を指定する
return item;
}


 最後に関数printItemを使っていた部分では、printItemそのものではなくItem構造体のメンバprintに設定された関数を使うようにします。

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

static void printItem(Item* item, int indent)
{



for (int i = 0; i < item->subItemCount; i++) {

Item* subitem = item->subItems[i];
subitem->print(item->subItems[i], indent); // subitemはsubitemのprintを使う
}
}



int main(int argc, const char * argv[])
{




// 印刷

root->print(root, 0); rootに設定されているprintを使う



}


 Runさせてもサンプル:01-01-folderと違いはありませんが、こちらはたとえ関数printItemがライブラリで提供されていても、Item構造体側のメンバprintを設定しなおす事で、ファイル名を [] 付きで出力できるようになります。
 またmain側のroot->print呼び出しは変更する必要はありません。
 
 実際にサンプル:folder-01-05のItem構造体関連関数をライブラリとし、これを拡張したサンプル:01-06-folderを用意したので参照してください。

サンプル:01-06-folder

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

static Item* initFileItem(Item* item, const char* name)
{



item->print = printFile; // 出力用に利用する関数を指定する
return item;
}

static void printFile(Item* item, int indent)
{
for (int i = 0; i < indent; i++) printf(" ");
printf("[%s]\n", item->name); // ファイルなので名前を [] で囲んでから表示
}

int main(int argc, const char * argv[])
{



Item* fileA = initFileItem(malloc(sizeof(Item)), "file A");
addItem(root, fileA);

// 印刷
root->print(root, 0); ←変更の必要がない


 main.c側に用意したprintFileにif文による分岐が必要なくなる事にも注目してください。

static void printFile(Item* item, int indent)
{
for (int i = 0; i < indent; i++) printf(" ");
printf("[%s]\n", item->name); ←無条件に [] 付きで出力している
}

 関数initFileItemを使って初期化するItem構造体だけ(すなわちファイル用のItem構造体だけ)が、構造体メンバprintに関数printFileが設定されるのでifによる分岐は必要ないのです。

テン*シー*シー-6

 実世界で赤ペンを線を引けば赤い線となり、黒ペンで線を引けば黒い線となるように、ディレクトリ用に作成されたItem構造体のprintを使えばディレクトリ用の表示になり、ファイル用に作成されたItem構造体のprintを使えばファイル用の表示となります。

テン*シー*シー-7

 この仕組みにより、ライブラリ側のprintItemは修正する事なくファイル用の出力に対応できる事になります。

テン*シー*シー-8

 メモリ上に確保された記憶区画である構造体を、実世界の物体のように振る舞わせるプログラミングであることから、このプログラミングスタイルに

オブジェクト(object:物、物体)指向プログラミング

という名前がついているわけです。
 これが、アプリケーション拡張時の問題点に対するオブジェクト指向プログラミングによる解決策の1例です。
 ところで、実世界の物体には

テン*シー*シー-9

といったように、オブジェクトの特性を継承した派生オブジェクトという概念が存在します。
 当然オブジェクト指向プログラミングでも、この概念が取り入れられました。

 次回はオブジェクトの派生をItem構造体を使って体験してみましょう。