株式会社エス・スリー・フォー

Cによるオブジェクト指向’風’プログラミング

オブジェクト指向'風'とは…

オブジェクト指向プログラミングのいわゆる三本柱:

  • 抽象データ型
  • 継承
  • 多態

は、それぞれがプログラムの堅牢さと保守性/拡張性を強力にサポートする概念です。

そしてJava/C++等のオブジェクト指向言語と称されるプログラミング言語はいずれも基本的にこの3つの概念を言語レベルでサポートしています。

一方、Cに代表される旧来の'手続き指向'言語では、上記の概念を言語レベルではサポートしていません。

しかしながら、コーディング・スタイルを工夫することによって、少なくとも抽象データ型については概ね代替案を示すことが可能です。

残る2つ、すなわち継承と多態をCで実現するには、関数へポインタ等の複雑な操作をプログラマに強いることになり、&不可能ではないが現実的ではない&と言えるでしょう。

Cによる抽象データ型

抽象データ型とは、

  • データの構造を外部に公開しない
  • データに対する参照/操作は必ず関数を介して行なう

の、2点を遵守することで実現されます。
すなわち、データをその構造や内容ではなく、それに対して摘要できる操作の集合で(抽象的に)データを定義するわけです。

こうすることによって、データの内容/構造が変化したとしても、その変化は参照/操作関数で吸収することができ、そのデータを利用する側への影響を小さく抑えることができ、堅牢かつ保守性/拡張性を向上させます。

例として、カード(トランプ)ゲームのベースとなる'カード'(Card)およびカードの一揃い'Pack'を考えます。

Packに用意する操作は、

shuffle 内包するカードをシャッフル(かき混ぜる)する
deal 一枚のカードを引く
replace (引いたカードを)Packに戻す

そしてCardに用意する操作は、

identifysuit スーツ(ハート/ダイヤ/クラブ/スペード)を返す
identifyvalue 値(2,3,4,….,10,jack,queen,king,ace)を返す

Java/C++ではオブジェクト・インスタンスにメッセージを送ってその結果を受け取るとき、

result instance.message(parameter);

のような記述となります。例えばPackからCardを一枚引き、Packに戻すJavaコードは:

  Pack pack = new Pack();
  Card card = pack.deal(); // Cardを一枚引く
  pack.replace(card); // (引いたCardを)packに戻す

同様のことをCで行なうためのスタイルを考えてましょう。

まず、抽象データはその詳細を外部に公開してはならないので、ヘッダには構造体のポインタのみを公開します。

typedef struct card_struct* card;

このとき、card_structの実際の構造(メンバ)はヘッダには明記せず、cardの実装部に置きます。

そして、cardに用意すべき操作(関数)の頭部に'card_'を付け、その後に操作名を続けます。さらに第一引数として操作の対象(オブジェクト)を与えます。

suit card_identifysuit(card);

また、すべてのオブジェクトには特殊操作すなわち、オブジェクトの生成と廃棄を行なう関数 create / destroy を用意します。生成関数 createに限り、操作対象オブジェクトを引数に与えず、関数の戻り値として生成されたオブジェクトを返します。

card card_create(suit, value);

void card_destory(card);

suit.h

/*
 * suit.h
 */

#ifndef __SUIT_H__
#define __SUIT_H__

typedef enum { hearts, clubs, diamonds, spades } suit;

#define suit_lowest()  hearts
#define suit_highest() spades
#define suit_next(s)   ((s)+1)

#endif

value.h

/*
 * value.h
 */

#ifndef __VALUE_H__
#define __VALUE_H__

typedef enum {
  two, three, four, five, six, seven, eight,
  nine, ten, jack, queen, king, ace
} value;

#define value_lowest()  two
#define value_highest() ace
#define value_next(v)   ((v)+1)

#endif

card.h

/*
 * card.h
 */

#ifndef __CARD_H__
#define __CARD_H__

#include "suit.h"
#include "value.h"

typedef struct card_struct* card;
/* 構造体card_structへのポインタcardを定義する。*/

card card_create(suit, value);
/* 新たなcardを生成する(生成に失敗したらNULL) */

void card_destory(card);
/* cardを廃棄する。*/

suit card_identifysuite(card);
/* cardのsuitを得る*/

value card_idenfiryvalue(card);
/* cardのvalueを得る*/

#endif

card.c

/*
 * card.c
 */

#include "card.h"

#include <stdlib.h>

struct card_struct {
  suit  s;
  value v;
};

card card_create(suit s, value v) {
  card c = (card) malloc(sizeof(struct card_struct));
  if ( c != NULL ) {
    c->s = s;
    c->v = v;
  }
  return c;
}

void card_destroy(card c) {
  free(c);
}

suit card_identifysuit(card c) {
  return c->s;
}

value card_identifyvalue(card c) {
  return c->v;
}

pack.h

/*
 * pack.h
 */

#ifndef __PACK_H__
#define __PACK_H__

#include "card.h"

typedef struct pack_struct* pack;
/* 構造体pack_structへのポインタ'pack'を定義する。
 * pack_struct の詳細は外部に公開しない
 */

#define PACK_MAXCARDS 52
/* packに含まれるcardの最大数*/

pack pack_create(void);
/* packを生成する。メモリ不足時にはNULLを返す。*/

void pack_destroy(pack);
/* packを廃棄する。この操作の後にpackを使用してはならない。*/

void pack_shuffle(pack);
/* pack内のcardをかき混ぜる。*/

card pack_deal(pack);
/* packからcardを一枚取り出す。pack内にcardが一枚もないときはNULL */

void pack_replace(pack, card);
/* cardをpackに戻す。*/

#endif

pack.c

/*
 * pack.c
 */

#include "pack.h"

#include <assert.h>
#include <stdlib.h>

struct pack_struct {
  int numberofcards;
  card cards[PACK_MAXCARDS];
};

pack pack_crate(void) {
  pack p;
  p = (pack)malloc(sizeof(struct pack_struct));
  if ( p != NULL ) {
    suit  s;
    value v;
    p->numberofcards = 0;
    for ( s = suit_lowest(); s <= suit_highest(); s = suit_next(s) ) {
      for ( v = value_lowest(); v <= value_highest(); v = value_next(v) ) {
        card c;
        c = card_create(s, v);
        if ( c == NULL ) {
          pack_destroy(p);
          return NULL;
        }
        pack_replace(p, c);
      }
    }
  }
  return p;
}

void pack_destory(pack p) {
  int n;
  for ( n = p->numberofcards-1; n >= 0; --n ) {
    card_destory(p->cards[n]);
  }
  free(p);
}

void pack_shufle(pack p) {
  int n;
  for ( n = p->numberofcards-1; n >= 0; --n ) {
    int swappos = rand() % (n+1);
    card tempcard = p->cards[n];
    p->cards[n] = p->cards[swappos];
    p->cards[swappos] = tempcard;
  }
}

card pack_deal(pack p) {
  if ( p->numberofcards == 0 ) {
    return NULL;
  } else {
    return p->cards[--p->numberofcards];
  }
}

void pack_replace(pack p, card c) {
  assert(p->numberofcards < PACK_MAXCARDS);
  p->cards[p->numberofcards++] = c;
}

このようなスタイルを採用した場合、PackからCardを一枚引き、Packに戻すCコードは次のようになります。

  pack p;
  card c;
  p = pack_create(); // packを生成する
  pack_shuffle(p); // packをかき混ぜる
  c = pack_deal(p); // packからcardを引く
  pack_replace(p, c); // cardをpackに戻す
  pack_destroy(p); // packを廃棄する

C++であれば次のようなコードになるでしょう。上のコードとの対比から、Cで抽象データ型で実現している様子が見て取れます。

  pack* p;
  card* c;
  p = new pack; // packを生成する
  p->shuffle(); // packをかき混ぜる
  c = p->deal(); // packからcardを引く
  p->replace(c); // cardをpackに戻す
  delete p; // packを廃棄する