雲風:私が好むC言語のオブジェクト向けプログラミングモデル

6035 ワード

オブジェクト向けプログラミングは銀弾ではありません.ほとんどの場合、私はオブジェクト向けの使用に非常に慎重で、使わなければ使わないことができます.関連する議論は展開されない.
しかし、場合によっては、オブジェクト向けを採用するのが確かに良い案です.UIフレームワーク、3 dレンダリングエンジンのシーン管理などです.C言語はオブジェクト向けプログラミングに対してオリジナルサポートはないが,オリジナルサポートがないとCでオブジェクト向けプログラムを書くのに適していないとは限らない.逆に、具体的な実現方法については、より多くの選択肢があります.
ほとんどのCでオブジェクト向けプログラムを書くプログラマーはC++の影響が深い.一般的なC++コンパイラで実装されているオブジェクトモデルをマクロでシミュレートしようとします.私の愚見では、これは良い方向ではありません.C++のオブジェクトモデルは,本質的に実装層の性能を追求し,直接体現するためである.C++で乱用されたinlineのように、確かに有効だが、分離の原則を破壊している.C++の継承は過密な結合である.
私が理解しているオブジェクト向けは,異なるデータ要素に共通の操作方式を持たせ,グループ化した処理に適している.操作方法によっては、データ要素を異なるグループ化します.1つのデータがこのグループに表示されるか、そのグループに表示される可能性があります.これはあなたが異なる面から抽出した共通性に依存します.これらの統一操作の共通性をインタフェース(Interface)と呼び,インタフェースはC言語で関数ポインタのセットとして表現される.C++に置くと、虚表になります.
私が好むオブジェクト向けの実装方法(C言語を使用)は、次のとおりです.
データのセットがあれば、fooという共通性があるように見せる必要があります.このようなデータに合致するものをfoo_objectと呼ぶ.通常、foo_objectを操作するapiがあります.
struct foo_object;



struct foo_object * foo_create();

void foo_release(struct foo_object *);

void foo_dosomething(struct foo_object *);

 
具体的に実現するとfooという名前になります.cの実装ファイルには、foo_object構造が定義されており、foo_dosomethingに必要なデータメンバーが含まれています.
しかし、以上は要求を満たすことができない.なぜなら、私たちは異なるデータを持っています.彼らはfoo_objectのいくつかの面の特性を示しているだけです.異なるデータに対してdosomething時に実際に行われる操作も異なります.この場合、fooのためのインタフェースを定義する必要があります.c内部使用.では、以上のヘッダファイルは、インタフェースi_fooの定義を追加し、create関数を変更する必要があります.
struct i_foo {

    void (*foobar)(void *);

};



struct foo_object * foo_create(struct i_foo *iface, void *data);

 
ここで少し説明します.i_fooは、foo_dosomethingの内部で使用されるインターフェースのセットである.foo_objectを構築するとき、外部データdataとfoo_objectの関連特性に定義されたi_fooインタフェースを結合し、構造関数foo_createに伝達する.一般的に、foo_objectの特性に適合するオブジェクトごとに、対応するi_fooを得る方法を実装します.
struct foobar;



struct i_foo * foobar_foo(void);

struct foobar * foobar_create(void);

void foobar_release(struct foobar *);
foo_objectオブジェクトを作成するコードは、次のように見えます.
struct foobar *foobar = foobar_create();

  struct foo_object * fobj = foo_create(foobar_foo() , foobar);
struct foo_objectの定義には、i_fooのインタフェースポインタとdataデータポインタが必ず記録される.C++の観点から、foo_objectはベースクラスであり、いくつかのベースクラスメンバーと非虚のメンバー関数もある.具体的な派生クラスは実装時に虚表i_fooの内容(虚関数を再ロード)を書き換えた.dataデータは、ベースクラスfoo_objectの継承時に拡張されたデータメンバーである.しかし,ここでは,メンバーを拡張するために組合せを用いた.これは間接性を増すが,より低い結合を提供する.その中の優劣はしばらく議論しない.
通常は次のように見えます.
struct foo_object {

    struct i_foo * vtbl;

    void * data;

    void * others;

};



void

foo_dosomething(struct foo_object *fobj)

{

    fobj->vtbl->foobar(fobj->data);

    // do something else

}

 
ここでもう一つの問題があります.dataの寿命は誰が責任を負うべきですか.
ライフサイクル管理は大きな課題です.C/C++を用いて開発されたソフトウェアの多くの複雑さの重要な源でもある.個人的にはライフタイム管理を独立して解決する傾向があります.したがって、foo_objectモジュールは、dataのライフサイクル管理を担当しないのが一般的です.struct foo_objectのリソースの解放のみを担当します.
自分で自分を経営するのは、私のC言語ソフトウェア開発の観点の一つです.私はハイブリッド言語のプログラミングを採用してこの問題をよりよく解決する傾向がある.例えばCとLua、あるいはCとC++です.ハイブリッド言語プログラミングを採用しない場合は、その後、同じC言語で作成された階層を増やして管理することもできます.この話題は、次回に残しておきます.
ライフサイクル管理を剥離することで、コード量を大幅に削減し、エラーを犯しにくくなります.
ps.C言語は弱いタイプの言語です.少なくともC++より弱いです.これは、
void*はC言語で任意のデータポインタを指すことができる.任意のデータポインタをvoid*変数に割り当てるか、void*変数を特定のポインタタイプ変数に割り当てることができます.(これはC++では推奨されず、コンパイラから警告されます)
C言語の関数ポインタも面白いです.通常、異なるタイプの関数ポインタが互いに値を割り当てると、コンパイラの警告が発生します(タイプが異なります).もちろん、void*で問題を解決することができます.しかし、タイプチェックを厳格にすることを望んでいます.少なくとも、1つのデータポインタを1つの関数ポインタに割り当てることを望んでいません.ただし、コンパイラは関数パラメータの違いを気にしないでください.
C言語では、void(*foo)()は、voidを返す任意の関数ポインタを付与することができる.すなわち、void foobar(int)のアドレスを前のfoo変数に与えることができます(これはC標準のパラメータ伝達規則によって保証されています).
したがって,C言語プログラミングでは注意が必要である.パラメータを受け入れない関数を定義し、コンパイラにパラメータを誤って渡した文をチェックさせたい場合は、パラメータを受け入れない関数を定義します.君はいなければならない.hファイルではvoid foo(void)を厳密に定義し、foo関数がパラメータを受け入れないことを示します.
従来のC言語では,構造の初期化に非常に注意が必要である.ここで,我々のi_fooインタフェース定義はC内の構造を用いた.これは非常に慎重に注意しなければならない.(C++コンパイラが手伝ってくれなかった)
C 99で新しく追加された構文は、この点を強化しています(構造を初期化する際に、順序に依存せずにメンバーの名前を書くことができます).採用に値する.
ここでは、http://blog.codingnow.com/2010/03/object_oriented_programming_in_c.htmlから