Python などのインタープリター言語のコアを形成するオブジェクトの実装とは対照的に、C 内から使用されることを意図したオブジェクトに特に興味があります。
6 に答える
私は次のようなことをする傾向があります:
struct foo_ops {
void (*blah)(struct foo *, ...);
void (*plugh)(struct foo *, ...);
};
struct foo {
struct foo_ops *ops;
/* data fields for foo go here */
};
これらの構造定義を使用すると、foo を実装するコードは次のようになります。
static void plugh(struct foo *, ...) { ... }
static void blah(struct foo *, ...) { ... }
static struct foo_ops foo_ops = { blah, plugh };
struct foo *new_foo(...) {
struct foo *foop = malloc(sizeof(*foop));
foop->ops = &foo_ops;
/* fill in rest of *foop */
return foop;
}
次に、foo を使用するコードで:
struct foo *foop = new_foo(...);
foop->ops->blah(foop, ...);
foop->ops->plugh(foop, ...);
このコードは、マクロまたはインライン関数で整理できるため、より C に似たものになります。
foo_blah(foop, ...);
foo_plugh(foop, ...);
ただし、「ops」フィールドに適度に短い名前を付ける場合は、最初に示したコードを単純に書き出すだけでは特に冗長ではありません。
この手法は、C で比較的単純なオブジェクト ベースの設計を実装するのに十分ですが、クラスの明示的な表現やメソッドの継承などの高度な要件には対応していません。それらについては、GObject のようなものが必要になるかもしれませんが (EFraim が述べたように)、より複雑なフレームワークの追加機能が本当に必要であることを確認することをお勧めします。
「オブジェクト」という用語の使用は少しあいまいなので、C を使用してオブジェクト指向プログラミングの特定の側面を達成する方法を尋ねていると仮定します (この仮定で私を修正してください.)
メソッドのポリモーフィズム:
メソッドのポリモーフィズムは、通常、関数ポインターを使用して C でエミュレートされます。たとえば、image_scaler (画像を取得して新しいサイズにサイズ変更するもの) を表すために使用する構造体がある場合、次のようなことができます。
struct image_scaler {
//member variables
int (*scale)(int, int, int*);
}
次に、いくつかの画像スケーラーを次のように作成できます。
struct image_scaler nn, bilinear;
nn->scale = &nearest_neighbor_scale;
bilinear->scale = &bilinear_scale;
これにより、image_scaler を取り込んで別の image_scaler を渡すだけでその scale メソッドを使用する任意の関数に対して、ポリモーフィックな動作を実現できます。
継承
継承は通常、次のように行われます。
struct base{
int x;
int y;
}
struct derived{
struct base;
int z;
}
これで、base のすべての「継承された」フィールドを取得するとともに、派生の追加フィールドを自由に使用できるようになりました。さらに、構造体ベースのみを受け取る関数がある場合。結果を伴わずに、構造体派生ポインターを構造体ベースポインターに単純にキャストできます
GObjectなどのライブラリ。
基本的に、GObject は不透明な値 (整数、文字列) とオブジェクトを記述する一般的な方法を提供します (インターフェイスを手動で記述することにより、基本的に C++ の VTable に対応する関数ポインタの構造として) - 構造の詳細については、リファレンスを参照してください。
「COM in plain C」のように、vtables を手動で実装することもよくあります。
すべての回答を閲覧するとわかるように、ライブラリ、関数ポインター、継承の手段、カプセル化など、すべて利用可能です (C++ はもともと C のフロントエンドでした)。
ただし、ソフトウェアにとって非常に重要な側面は読みやすさであることがわかりました。10 年前のコードを読み取ろうとしたことがありますか? その結果、C でオブジェクトのようなことを行うときは、最も単純なアプローチを取る傾向があります。
次のように尋ねます。
- これは期限のある顧客向けですか (そうであれば、OOP を検討してください)?
- OOP を使用できますか (多くの場合、コードが少なくなり、開発が速くなり、読みやすくなります)?
- ライブラリ (既存のコード、既存のテンプレート) を使用できますか?
- メモリや CPU (Arduino など) の制約を受けますか?
- C を使用する別の技術的な理由はありますか?
- C を非常に単純で読みやすいものに保つことができますか?
- 自分のプロジェクトに本当に必要な OOP 機能は何ですか?
私は通常、コードをカプセル化し、非常に読みやすいインターフェイスを提供する GLIB API のようなものに戻ります。さらに必要な場合は、ポリモーフィズム用の関数ポインターを追加します。
class_A.h:
typedef struct _class_A {...} Class_A;
Class_A* Class_A_new();
void Class_A_empty();
...
#include "class_A.h"
Class_A* my_instance;
my_instance = Class_A_new();
my_instance->Class_A_empty(); // can override using function pointers
Dale のアプローチに似ていますが、PostgreSQL が構文解析ツリー ノード、式の型などを内部的に表現する方法がもう少し重要です。次の行に沿って、デフォルトNode
と構造体があります。Expr
typedef struct {
NodeTag n;
} Node;
whereNodeTag
は unsigned int の typedef であり、考えられるすべてのノード タイプを記述した一連の定数を含むヘッダー ファイルがあります。ノード自体は次のようになります。
typedef struct {
NodeTag n = FOO_NODE;
/* other members go here */
} FooNode;
また、 C 構造体の癖により、aFooNode
を a にキャストしてもNode
問題ありません。2 つの構造体の最初のメンバーが同じ場合、それらは互いにキャストできます。
はい、これは aFooNode
を a にキャストできることを意味しますがBarNode
、これはおそらくやりたくないでしょう。適切な実行時の型チェックが必要な場合は、GObject が最適ですが、コツをつかんでいる間は人生を嫌うことを覚悟してください。
(注: 記憶からの例です。私はしばらく Postgres の内部をハッキングしていません。開発者の FAQに詳細情報があります。)
IJG の実装を見てください。例外処理に setjmp/longjmp を使用するだけでなく、vtables などすべてを備えています。これはよく書かれた、非常に良い例を得るのに十分小さいライブラリです。