C言語とオブジェクト指向

概要

C言語でプログラムを書く際によくある

  • 数百行あるような関数や点在する同じような処理
  • get2()get3() のように全体としては同じような関数なのに中の一部だけが異なるためにす数字で区別をしている関数
  • なんとか内部処理を共通化できているものの、動作を制御するために大量の引数が必要な関数
  • 使用するデータがグローバル変数でつながっていて、独立性が皆無の関数群
  • お互いに依存しあっていて、個々にテストするのが困難な関数群

など.プログラムを構造化したはずなのになぜか生まれてしまう.

このようなときにデザインパターンを利用できる.デザインパターンはオブジェクト指向言語を前提としているが、うまくやればC言語でも可能.

Cのモジュール化とオブジェクト指向

Cとモジュール化(stack1,stack2)

一番単純な実装.stackに利用するデータをグローバル変数として定義し,そのデータを叩くことを前提に push()pop() を定義する。またそのimplで利用する isStackFull()isStackEmpty()static 指定しておき,stack.c 内部でのみ利用する。しかし複数のstackを利用することはできない.

構造体によるデータ構造とロジックの分離(tack3)

データを構造体にひとまとめにすることで複数のstackを簡単に構築できる.

typedef struct {
  int top;
  const size_t size;
  int *pBuff;
} Stack;

bool push(Stack *p, int val);
bool pop(Stack *p, int *pRet);

また以下のようなマクロによりC言語でもC++言語のコンストラクタのような初期化ができる。

#define newStack(buf){                      \
0, sizeof(buf) / sizeof(int), (buf)         \
}

Cを用いたオブジェクト指向

これからやることは,stackに push() する値をチェックするチェッカーを持たせ,特定の条件を満たすものだけを通すこと,まず始めに 特定の範囲内の値だけ通す チェッカー機能を持たせ,次に 任意の条件を満たすチェッカー機能 へと抽象化する.

一般にチェッカーは

  • その値(ここではint)を引数に取り
  • boolを返す関数

により実現される(trueを返したらpushできる)。またチェッカーの種類によってはチェック自体にデータが必要になることがある(例えば範囲を指定するときの上限と下限など)。そこでこの関数オブジェクトのようなものをデータへのvoidポインタと関数ポインタを持つ構造体`Validator`により実現する。

チェック機能付きスタック(stack4)

構造体にpushする値の範囲をデータとして持たせて,pushするときに利用する.

範囲チェック付きスタックの問題点(stack5)

前項でつくったstackは

  • 範囲チェックなしのスタックを生成した場合にも needRangeCheckminmax といったメンバ変数を持つ必要があり,メモリが無駄に消費される
  • スタック内にこれとは別のチェック機能を持たせたいと思った時に,さらに別のデータを追加しないといけないため,各インスタンスが使わない機能のためのデータを余計に持つことになる

という問題がある.とりあえずそのデータへのポインタを保持するようにすることで無駄なデータを持たなくて良いようにする.

チェック機能を汎用化する(stack6)

値のチェックを行う機能としては何も上下限だけとは限らない.例えば前回pushした値以上の値しかpushできない(ほんとに?とはなるが例として)ものも考えられる。そういう任意のチェックを行えるよう,より抽象化を行う.

オブジェクト指向と多態性

オブジェクト指向のポイントは、データとその処理を 両方とも まとめるところにある。Validatorという構造体の中に検証の処理(関数ポインタvalidate)と、その検証処理が使用するデータ(voidポインタpData)とをセットで切り出すことにより,検証処理が独立し,スタックの中にさまざまな検証処理をあとから付け加えることができるようになった.

オブジェクト指向の要件として他には,多態性が挙げられる.ここまでだと,push関数は値を検証するのにValidator->validateを利用するのみで,validateが実際に指している関数の中身は一切関知していない.

継承(stack7)

オブジェクト指向の重要な概念の一つとして継承がある.先ほどの例だと、もともとのValidatorがあり,それを拡張した(とはいっても関数ポインタを代入しただけだが)範囲チェックValidator,直前の値との検証Validatorという2つのValidatorが存在している.この場合、元々のValidatorのことを親、拡張したValidatorを子と呼ぶ.

先ほどの範囲チェックValidatorではpDataの指す先がRangeになっている.このような方は申し少し複雑になってくると面倒なことになる.例えばこの範囲チェックValidatorを拡張して,偶数あるいは奇数しか受け付けないようなValidatorを作るとする。この場合単純にRangeを拡張してしまうと

typedef struct{
 const int min;
 const int max;
 const bool needOddEvenCheck; // trueなら偶奇チェックする
 const boll needToBeEven;     // trueなら奇数でなければいけない
} Range;

Rangeの位置づけが曖昧になる。Rangeは「範囲」という意味であるからここに偶奇のチェックが入るのは変だし、そのチェックが不要な場合は余計なメンバを2つ抱えることになってしまう。これをうまく解決するため、継承っぽいことをやってみる。

カプセル化

カプセル化はオブジェクトの持つ状態と振る舞いを一箇所に集め,外部とのインターフェイスを規定することで抽象化することを指す.ここで状態とは構造体の中の関数ポインタ以外のメンバ,振る舞いは関数ポインタが指している関数の動作.

fopenしてからそのファイルポインタをfreadし,最後にfcloseするみたいなことはよくあるが,Cプログラマは普通FILE構造体の中身については気にしない.そもそも <stdio.h> にかかれている実装内容は処理系により異なっており,ユーザーとしては仕様書に書かれているインターフェースだけが重要.implで使われているメンバや関数(staticにするのでは?)がどういう意味を持つのか知る必要もない.

オブジェクト指向言語ではデータを隠蔽する仕組みがあるが(C++のprivateやprotected)、C言語にはない.そういう場合はconst指定して書き換えないようにするとか,アンダースコア \_ で始まるメンバは読み書きしないみたいな規則を設ける.

仮想関数テーブル

オブジェクトが保有する関数ポインタ自体が,メモリの無駄になる可能性がある.

tyepdef struct Foo {
 const int count;
 void (*func0)(struct Foo *This);
 void (*func1)(struct Foo *This);
 void (*func2)(strcut Foo *This);
} Foo;

このようなオブジェクトを複数インスタンス化したとする。

Foo foo0 = {0, func0_impl, func1_impl, func2_impl};
Foo foo1 = {1, func0_impl, func1_impl, func2_impl};
Foo foo2 = {2, func0_impl, func1_impl, func2_impl};

この場合関数ポインタがメモリを無駄に使用しかねない。そこで仮想関数テーブルを導入することでこの無駄を排除できる.

typedef struct FooVtbl{
 void (*func0)(struct Foo *This);
 void (*func1)(struct Foo *This);
 void (*func2)(struct Foo *This);
} FooVtbl;

static FooVtbl foo_vtbl = {func0_impl, func1_impl, func2_impl};

typedef struct Foo{
 const int count;
 const FooVtbl *vptr;
} Foo;

Foo foo0 = {0, &foo_vtbl};
Foo foo1 = {1, &foo_vtbl};
Foo foo2 = {2, &foo_vtbl}

一方関数をcallするときはこの仮想関数テーブルを経由しないといけないので,記述が面倒になりうる.

Foo *pFoo;
pFoo->vptr->func(pFoo);

まとめ

オブジェクト指向のエッセンスは多態,継承,カプセル化.

  • 多態を用いることで,振る舞いの異なるオブジェクトを同じように扱えるようになる
  • 継承は、一部のみが異なるコードの共通部分を取り出すことを容易にする
  • カプセル化によりオブジェクトの振る舞いと内部状態を一箇所に集めて抽象化を進め,扱いを容易にする

GUIライブラリなどではたとえC言語であってもオブジェクト指向的な設計になっていたりする.