Cで何らかのい(しかし使用可能な)オブジェクト指向を可能にする気の利いたプリプロセッサハック(ANSI C89/ISO C90互換)のセットは何でしょうか?
私はいくつかの異なるオブジェクト指向言語に精通しているので、「Learn C++!」のような答えを返さないでください。 「 ANSI Cを使用したオブジェクト指向プログラミング 」(注意:PDF形式)および他のいくつかの興味深いソリューションを読みましたが、私は主にあなたのものに興味があります:-)!
Cでオブジェクト指向コードを記述できますか?も参照してください。
C Object System(COS) 有望に聞こえます(まだアルファ版です)。シンプルさと柔軟性のために、使用可能な概念を最小限に抑えようとしています。オープンクラス、メタクラス、プロパティメタクラス、ジェネリック、マルチメソッド、委任、所有権、例外、コントラクト、クロージャを含む統一オブジェクト指向プログラミング。 ドラフトペーパー (PDF)があります。
Cの例外 は、他のOO言語にあるTRY-CATCH-FINALLYのC89実装です。テストスイートといくつかの例が付属しています。
両方とも CのOOP で多くの作業をしているLaurent Deniauによるものです。
プリプロセッサ(ab)の使用に対して、C構文を他のオブジェクト指向言語の構文に似せようとすることをお勧めします。最も基本的なレベルでは、単純な構造体をオブジェクトとして使用し、それらをポインターで渡すだけです。
struct monkey
{
float age;
bool is_male;
int happiness;
};
void monkey_dance(struct monkey *monkey)
{
/* do a little dance */
}
継承やポリモーフィズムなどを取得するには、もう少し努力する必要があります。構造体の最初のメンバーをスーパークラスのインスタンスにすることで手動継承を実行できます。その後、ベースクラスと派生クラスへのポインターを自由にキャストできます。
struct base
{
/* base class members */
};
struct derived
{
struct base super;
/* derived class members */
};
struct derived d;
struct base *base_ptr = (struct base *)&d; // upcast
struct derived *derived_ptr = (struct derived *)base_ptr; // downcast
ポリモーフィズム(仮想関数)を取得するには、関数ポインターを使用し、オプションで関数ポインターテーブル(仮想テーブルまたはvtableとも呼ばれる)を使用します。
struct base;
struct base_vtable
{
void (*dance)(struct base *);
void (*jump)(struct base *, int how_high);
};
struct base
{
struct base_vtable *vtable;
/* base members */
};
void base_dance(struct base *b)
{
b->vtable->dance(b);
}
void base_jump(struct base *b, int how_high)
{
b->vtable->jump(b, how_high);
}
struct derived1
{
struct base super;
/* derived1 members */
};
void derived1_dance(struct derived1 *d)
{
/* implementation of derived1's dance function */
}
void derived1_jump(struct derived1 *d, int how_high)
{
/* implementation of derived 1's jump function */
}
/* global vtable for derived1 */
struct base_vtable derived1_vtable =
{
&derived1_dance, /* you might get a warning here about incompatible pointer types */
&derived1_jump /* you can ignore it, or perform a cast to get rid of it */
};
void derived1_init(struct derived1 *d)
{
d->super.vtable = &derived1_vtable;
/* init base members d->super.foo */
/* init derived1 members d->foo */
}
struct derived2
{
struct base super;
/* derived2 members */
};
void derived2_dance(struct derived2 *d)
{
/* implementation of derived2's dance function */
}
void derived2_jump(struct derived2 *d, int how_high)
{
/* implementation of derived2's jump function */
}
struct base_vtable derived2_vtable =
{
&derived2_dance,
&derived2_jump
};
void derived2_init(struct derived2 *d)
{
d->super.vtable = &derived2_vtable;
/* init base members d->super.foo */
/* init derived1 members d->foo */
}
int main(void)
{
/* OK! We're done with our declarations, now we can finally do some
polymorphism in C */
struct derived1 d1;
derived1_init(&d1);
struct derived2 d2;
derived2_init(&d2);
struct base *b1_ptr = (struct base *)&d1;
struct base *b2_ptr = (struct base *)&d2;
base_dance(b1_ptr); /* calls derived1_dance */
base_dance(b2_ptr); /* calls derived2_dance */
base_jump(b1_ptr, 42); /* calls derived1_jump */
base_jump(b2_ptr, 42); /* calls derived2_jump */
return 0;
}
そして、それがCでポリモーフィズムを行う方法です。それはきれいではありませんが、仕事をします。基本クラスと派生クラス間のポインターキャストに関連するいくつかのスティッキーな問題があります。これは、基本クラスが派生クラスの最初のメンバーである限り安全です。多重継承ははるかに困難です-その場合、最初のクラス以外の基本クラス間でケースを行うには、適切なオフセットに基づいてポインターを手動で調整する必要があります。これは本当にトリッキーでエラーが発生しやすいです。
実行できるもう1つの(厄介な)ことは、実行時にオブジェクトの動的タイプを変更することです!新しいvtableポインターを再割り当てするだけです。仮想機能の一部を選択的に変更し、その他を維持して、新しいハイブリッドタイプを作成することもできます。グローバルvtableを変更するのではなく、新しいvtableを作成するように注意してください。そうしないと、指定されたタイプのすべてのオブジェクトに誤って影響を与えます。
かつて私は非常にエレガントであるように思われる方法で実装されたCライブラリを使用していました。彼らはCでオブジェクトを定義し、それらから継承してC++オブジェクトと同じように拡張できる方法を書いていました。基本的なアイデアはこれでした:
継承を説明することは困難ですが、基本的には次のとおりです。
struct vehicle {
int power;
int weight;
}
次に、別のファイルで:
struct van {
struct vehicle base;
int cubic_size;
}
次に、メモリ内にバンを作成し、車両のみを知っているコードで使用することができます。
struct van my_van;
struct vehicle *something = &my_van;
vehicle_function( something );
それは見事に機能し、.hファイルは各オブジェクトでできることを正確に定義しました。
Linux用のGNOMEデスクトップはオブジェクト指向Cで書かれており、プロパティ、継承、ポリモーフィズム、および参照、イベント処理などの他の機能をサポートする「 GObject 」というオブジェクトモデルを備えています。 「シグナル」と呼ばれます)、ランタイムタイピング、プライベートデータなど。
これには、クラス階層での型キャストなどを行うためのプリプロセッサハックが含まれています。これは、GNOME用に作成したクラスの例です(gcharなどはtypedefです)。
GObject構造内には、GLibの動的型付けシステムのマジックナンバーとして使用されるGType整数があります(構造体全体を「GType」にキャストして型を見つけることができます)。
オブジェクトで呼び出されるメソッドを、暗黙的な 'this
'を関数に渡す静的メソッドと考えると、CでのOOの考え方が容易になります。
例えば:
String s = "hi";
System.out.println(s.length());
になる:
string s = "hi";
printf(length(s)); // pass in s, as an implicit this
またはそのようなもの。
OOPが何であるかを知る前に、私はCでこの種のことをしていました。
次に、最小サイズ、増分、および最大サイズを指定して、オンデマンドで成長するデータバッファを実装する例を示します。この特定の実装は「要素」ベースでした。つまり、可変長のバイトバッファだけでなく、あらゆるCタイプのリストのようなコレクションを可能にするように設計されました。
オブジェクトはxxx_crt()を使用してインスタンス化され、xxx_dlt()を使用して削除されるという考え方です。 「メンバー」メソッドのそれぞれは、操作するために特別に型指定されたポインターを受け取ります。
この方法で、リンクリスト、循環バッファ、およびその他の多くのものを実装しました。
私は告白しなければなりません。このアプローチで継承を実装する方法について考えたことは一度もありません。 Kieveliが提供するもののいくつかのブレンドが良い道かもしれないと思います。
dtb.c:
#include <limits.h>
#include <string.h>
#include <stdlib.h>
static void dtb_xlt(void *dst, const void *src, vint len, const byte *tbl);
DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz) {
DTABUF *dbp;
if(!minsiz) { return NULL; }
if(!incsiz) { incsiz=minsiz; }
if(!maxsiz || maxsiz<minsiz) { maxsiz=minsiz; }
if(minsiz+incsiz>maxsiz) { incsiz=maxsiz-minsiz; }
if((dbp=(DTABUF*)malloc(sizeof(*dbp))) == NULL) { return NULL; }
memset(dbp,0,sizeof(*dbp));
dbp->min=minsiz;
dbp->inc=incsiz;
dbp->max=maxsiz;
dbp->siz=minsiz;
dbp->cur=0;
if((dbp->dta=(byte*)malloc((vuns)minsiz)) == NULL) { free(dbp); return NULL; }
return dbp;
}
DTABUF *dtb_dlt(DTABUF *dbp) {
if(dbp) {
free(dbp->dta);
free(dbp);
}
return NULL;
}
vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen) {
if(!dbp) { errno=EINVAL; return -1; }
if(dtalen==-1) { dtalen=(vint)strlen((byte*)dtaptr); }
if((dbp->cur + dtalen) > dbp->siz) {
void *newdta;
vint newsiz;
if((dbp->siz+dbp->inc)>=(dbp->cur+dtalen)) { newsiz=dbp->siz+dbp->inc; }
else { newsiz=dbp->cur+dtalen; }
if(newsiz>dbp->max) { errno=ETRUNC; return -1; }
if((newdta=realloc(dbp->dta,(vuns)newsiz))==NULL) { return -1; }
dbp->dta=newdta; dbp->siz=newsiz;
}
if(dtalen) {
if(xlt256) { dtb_xlt(((byte*)dbp->dta+dbp->cur),dtaptr,dtalen,xlt256); }
else { memcpy(((byte*)dbp->dta+dbp->cur),dtaptr,(vuns)dtalen); }
dbp->cur+=dtalen;
}
return 0;
}
static void dtb_xlt(void *dst,const void *src,vint len,const byte *tbl) {
byte *sp,*dp;
for(sp=(byte*)src,dp=(byte*)dst; len; len--,sp++,dp++) { *dp=tbl[*sp]; }
}
vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...) {
byte textÝ501¨;
va_list ap;
vint len;
va_start(ap,format); len=sprintf_len(format,ap)-1; va_end(ap);
if(len<0 || len>=sizeof(text)) { sprintf_safe(text,sizeof(text),"STRTOOLNG: %s",format); len=(int)strlen(text); }
else { va_start(ap,format); vsprintf(text,format,ap); va_end(ap); }
return dtb_adddta(dbp,xlt256,text,len);
}
vint dtb_rmvdta(DTABUF *dbp,vint len) {
if(!dbp) { errno=EINVAL; return -1; }
if(len > dbp->cur) { len=dbp->cur; }
dbp->cur-=len;
return 0;
}
vint dtb_reset(DTABUF *dbp) {
if(!dbp) { errno=EINVAL; return -1; }
dbp->cur=0;
if(dbp->siz > dbp->min) {
byte *newdta;
if((newdta=(byte*)realloc(dbp->dta,(vuns)dbp->min))==NULL) {
free(dbp->dta); dbp->dta=null; dbp->siz=0;
return -1;
}
dbp->dta=newdta; dbp->siz=dbp->min;
}
return 0;
}
void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen) {
if(!elmlen || (elmidx*elmlen)>=dbp->cur) { return NULL; }
return ((byte*)dbp->dta+(elmidx*elmlen));
}
dtb.h
typedef _Packed struct {
vint min; /* initial size */
vint inc; /* increment size */
vint max; /* maximum size */
vint siz; /* current size */
vint cur; /* current data length */
void *dta; /* data pointer */
} DTABUF;
#define dtb_dtaptr(mDBP) (mDBP->dta)
#define dtb_dtalen(mDBP) (mDBP->cur)
DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz);
DTABUF *dtb_dlt(DTABUF *dbp);
vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen);
vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...);
vint dtb_rmvdta(DTABUF *dbp,vint len);
vint dtb_reset(DTABUF *dbp);
void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen);
PS:vintは単純にintのtypedefでした-長さはプラットフォームごとに異なることを思い出させるために使用しました(移植のため)。
ffmpeg (ビデオ処理用のツールキット)は、ストレートC(およびアセンブリ言語)で記述されていますが、オブジェクト指向のスタイルを使用しています。関数ポインターを持つ構造体でいっぱいです。適切な「メソッド」ポインタで構造体を初期化するファクトリ関数のセットがあります。
本当に丁寧に考えているなら、標準CライブラリでさえOOP-例としてFILE *
を考慮してください:fopen()
はFILE *
オブジェクトを初期化し、メンバーメソッドfscanf()
、fprintf()
、fread()
、fwrite()
などを使用し、最終的にfclose()
でファイナライズします。
また、同様に難しくない擬似目的Cの方法で行くことができます:
typedef void *Class;
typedef struct __class_Foo
{
Class isa;
int ivar;
} Foo;
typedef struct __meta_Foo
{
Foo *(*alloc)(void);
Foo *(*init)(Foo *self);
int (*ivar)(Foo *self);
void (*setIvar)(Foo *self);
} meta_Foo;
meta_Foo *class_Foo;
void __meta_Foo_init(void) __attribute__((constructor));
void __meta_Foo_init(void)
{
class_Foo = malloc(sizeof(meta_Foo));
if (class_Foo)
{
class_Foo = {__imp_Foo_alloc, __imp_Foo_init, __imp_Foo_ivar, __imp_Foo_setIvar};
}
}
Foo *__imp_Foo_alloc(void)
{
Foo *foo = malloc(sizeof(Foo));
if (foo)
{
memset(foo, 0, sizeof(Foo));
foo->isa = class_Foo;
}
return foo;
}
Foo *__imp_Foo_init(Foo *self)
{
if (self)
{
self->ivar = 42;
}
return self;
}
// ...
使用するには:
int main(void)
{
Foo *foo = (class_Foo->init)((class_Foo->alloc)());
printf("%d\n", (foo->isa->ivar)(foo)); // 42
foo->isa->setIvar(foo, 60);
printf("%d\n", (foo->isa->ivar)(foo)); // 60
free(foo);
}
これは、かなり古いObjective-C-to-Cトランスレーターが使用されている場合、次のようないくつかのObjective-Cコードから生じる可能性があります。
@interface Foo : NSObject
{
int ivar;
}
- (int)ivar;
- (void)setIvar:(int)ivar;
@end
@implementation Foo
- (id)init
{
if (self = [super init])
{
ivar = 42;
}
return self;
}
@end
int main(void)
{
Foo *foo = [[Foo alloc] init];
printf("%d\n", [foo ivar]);
[foo setIvar:60];
printf("%d\n", [foo ivar]);
[foo release];
}
Adam Rosenfieldが投稿したのは、CでOOPを行う正しい方法だと思います。彼が示すのはオブジェクトの実装です。つまり、実際の実装は.c
ファイル、インターフェースはヘッダー.h
ファイル。たとえば、上記の猿の例を使用します。
インターフェイスは次のようになります。
//monkey.h
struct _monkey;
typedef struct _monkey monkey;
//memory management
monkey * monkey_new();
int monkey_delete(monkey *thisobj);
//methods
void monkey_dance(monkey *thisobj);
インターフェイスで見ることができます.h
ファイルは、プロトタイプのみを定義しています。その後、実装部分「.c
file "を静的または動的ライブラリに追加します。これにより、カプセル化が作成され、実装を自由に変更することもできます。オブジェクトのユーザーは、その実装についてほとんど何も知る必要がありません。オブジェクト。
Oopはコード構造と再利用性を概念化する方法であり、オーバーロードやテンプレートのようなc ++に追加された他のこととはまったく関係がないというのが私の個人的な信念です。はい、これらは非常に便利な機能ですが、オブジェクト指向プログラミングが実際に何であるかを代表するものではありません。
Cを使用してオブジェクト指向スタイルでプログラミングする別の方法は、ドメイン固有の言語をCに変換するコードジェネレーターを使用することです。TypeScriptとJavaScriptを使用して、OOP=をjs.
私の推奨事項:シンプルにしてください。私が抱えている最大の問題の1つは、古いソフトウェア(場合によっては10年以上)の保守です。コードが単純ではない場合、困難になる可能性があります。はい、非常に便利なOOPをCのポリモーフィズムで記述できますが、読みにくい場合があります。
いくつかの明確に定義された機能をカプセル化したシンプルなオブジェクトが好きです。これのすばらしい例は GLIB2 で、たとえばハッシュテーブルです:
GHastTable* my_hash = g_hash_table_new(g_str_hash, g_str_equal);
int size = g_hash_table_size(my_hash);
...
g_hash_table_remove(my_hash, some_key);
キーは次のとおりです。
オープンソースのDynaceプロジェクトはまさにそれを行います。 https://github.com/blakemcbride/Dynace にあります
また、マクロソリューションに基づいてこれに取り組んでいます。だから、それは勇敢な人のためだけだと思う;-)しかし、それはすでにかなりいいです、そして私はすでにその上でいくつかのプロジェクトに取り組んでいます。これは、最初にクラスごとに個別のヘッダーファイルを定義するように機能します。このような:
_#define CLASS Point
#define BUILD_JSON
#define Point__define \
METHOD(Point,public,int,move_up,(int steps)) \
METHOD(Point,public,void,draw) \
\
VAR(read,int,x,JSON(json_int)) \
VAR(read,int,y,JSON(json_int)) \
_
クラスを実装するには、クラスのヘッダーファイルと、メソッドを実装するCファイルを作成します。
_METHOD(Point,public,void,draw)
{
printf("point at %d,%d\n", self->x, self->y);
}
_
クラス用に作成したヘッダーに、必要な他のヘッダーを含め、クラスに関連するタイプなどを定義します。クラスヘッダーとCファイルの両方に、クラス仕様ファイル(最初のコード例を参照)とXマクロを含めます。これらのXマクロ( 1 、 2 、 3 など)は、実際のクラス構造体およびその他の宣言にコードを展開します。
クラスを継承するには、_#define SUPER supername
_をクラス定義の最初の行として_supername__define \
_を追加します。両方が存在する必要があります。 JSONサポート、シグナル、抽象クラスなどもあります。
オブジェクトを作成するには、W_NEW(classname, .x=1, .y=2,...)
を使用します。初期化は、C11で導入された構造体の初期化に基づいています。うまく機能し、リストされていないものはすべてゼロに設定されます。
メソッドを呼び出すには、W_CALL(o,method)(1,2,3)
を使用します。高階関数呼び出しのように見えますが、これは単なるマクロです。それは_((o)->klass->method(o,1,2,3))
_に展開され、これは本当に素晴らしいハックです。
フレームワークには定型的なコードが必要なので、この仕事をするPerlスクリプト(wobject)を作成しました。それを使うなら、あなたはただ書くことができます
_class Point
public int move_up(int steps)
public void draw()
read int x
read int y
_
クラス仕様ファイル、クラスヘッダー、およびクラスを実装する_Point_impl.c
_を含むCファイルを作成します。多くの単純なクラスがあり、それでもすべてがCにある場合、かなりの作業を節約できます。 wobject は非常に単純な正規表現ベースのスキャナーです。特定のニーズに簡単に適応したり、ゼロから書き直したりできます。
#include "triangle.h"
#include "rectangle.h"
#include "polygon.h"
#include <stdio.h>
int main()
{
Triangle tr1= CTriangle->new();
Rectangle rc1= CRectangle->new();
tr1->width= rc1->width= 3.2;
tr1->height= rc1->height= 4.1;
CPolygon->printArea((Polygon)tr1);
printf("\n");
CPolygon->printArea((Polygon)rc1);
}
出力:
6.56
13.12
以下は、OO Cを使用したプログラミングです。
これは実際の純粋なCであり、プリプロセッサマクロはありません。継承、ポリモーフィズム、およびデータのカプセル化(クラスまたはオブジェクトにプライベートなデータを含む)があります。同等の保護された修飾子が存在する可能性はありません。つまり、プライベートデータも継承チェーンの下位にあります。しかし、これは必要ではないと思うので不便ではありません。
CPolygon
はインスタンス化されません。これは、共通の側面を持ちながら実装が異なる継承チェーンのオブジェクトを操作するためにのみ使用するためです(多態性)。
私にとって、Cのオブジェクト指向には次の機能が必要です。
カプセル化とデータの非表示(構造体/不透明ポインターを使用して実現可能)
多相性の継承とサポート(単一の継承は構造体を使用して実現できます-抽象ベースがインスタンス化できないことを確認してください)
コンストラクターとデストラクターの機能(簡単に実現できない)
型チェック(少なくともCは何も強制しないため、ユーザー定義型の場合)
参照カウント(または実装するもの [〜#〜] raii [〜#〜] )
例外処理の限定サポート(setjmpおよびlongjmp)
上記に加えて、ANSI/ISO仕様に依存する必要があり、コンパイラ固有の機能に依存するべきではありません。
http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html をご覧ください。ドキュメントを読むことが他に何もなければ、啓発的な経験になります。
私はここでパーティーに少し遅れていますが、両方マクロの極端さを避けるのが好きです-コードが多すぎるまたは難読化するが、いくつかの明らかなマクロはOOPコードをより簡単にすることができます開発して読む:
/*
* OOP in C
*
* gcc -o oop oop.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
struct obj2d {
float x; // object center x
float y; // object center y
float (* area)(void *);
};
#define X(obj) (obj)->b1.x
#define Y(obj) (obj)->b1.y
#define AREA(obj) (obj)->b1.area(obj)
void *
_new_obj2d(int size, void * areafn)
{
struct obj2d * x = calloc(1, size);
x->area = areafn;
// obj2d constructor code ...
return x;
}
// --------------------------------------------------------
struct rectangle {
struct obj2d b1; // base class
float width;
float height;
float rotation;
};
#define WIDTH(obj) (obj)->width
#define HEIGHT(obj) (obj)->height
float rectangle_area(struct rectangle * self)
{
return self->width * self->height;
}
#define NEW_rectangle() _new_obj2d(sizeof(struct rectangle), rectangle_area)
// --------------------------------------------------------
struct triangle {
struct obj2d b1;
// deliberately unfinished to test error messages
};
#define NEW_triangle() _new_obj2d(sizeof(struct triangle), triangle_area)
// --------------------------------------------------------
struct circle {
struct obj2d b1;
float radius;
};
#define RADIUS(obj) (obj)->radius
float circle_area(struct circle * self)
{
return M_PI * self->radius * self->radius;
}
#define NEW_circle() _new_obj2d(sizeof(struct circle), circle_area)
// --------------------------------------------------------
#define NEW(objname) (struct objname *) NEW_##objname()
int
main(int ac, char * av[])
{
struct rectangle * obj1 = NEW(rectangle);
struct circle * obj2 = NEW(circle);
X(obj1) = 1;
Y(obj1) = 1;
// your decision as to which of these is clearer, but note above that
// macros also hide the fact that a member is in the base class
WIDTH(obj1) = 2;
obj1->height = 3;
printf("obj1 position (%f,%f) area %f\n", X(obj1), Y(obj1), AREA(obj1));
X(obj2) = 10;
Y(obj2) = 10;
RADIUS(obj2) = 1.5;
printf("obj2 position (%f,%f) area %f\n", X(obj2), Y(obj2), AREA(obj2));
// WIDTH(obj2) = 2; // error: struct circle has no member named width
// struct triangle * obj3 = NEW(triangle); // error: triangle_area undefined
}
これはバランスがとれていると思いますし、(少なくともデフォルトのgcc 6.3オプションで)発生する可能性のあるいくつかのエラーに対して生成されるエラーは、混乱を招く代わりに役立ちます。全体のポイントは、プログラマーの生産性を向上させることです。