171

C である種の見苦しい (しかし使用可能な) オブジェクト指向を可能にする一連の気の利いたプリプロセッサ ハック (ANSI C89/ISO C90 互換) は何でしょうか?

私はいくつかの異なるオブジェクト指向言語に精通しているので、「C++ を学ぼう!」などの回答はしないでください。「 ANSI C を使用したオブジェクト指向プログラミング」(注意: PDF 形式) と他のいくつかの興味深いソリューションを読みましたが、主にあなたのソリューションに興味があります :-)!


オブジェクト指向のコードを C で書けますか?も参照してください。

4

22 に答える 22

199

C 構文を別のよりオブジェクト指向の言語の構文に近づけようとするために、プリプロセッサを (ab) 使用しないことをお勧めします。最も基本的なレベルでは、単純な構造体をオブジェクトとして使用し、それらをポインターで渡します。

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 を作成するように注意してください。そうしないと、特定のタイプのすべてのオブジェクトに誤って影響を与えることになります。

于 2009-01-06T05:09:55.010 に答える
35

私は以前、非常に洗練された方法で実装された C ライブラリを使用したことがありました。彼らは C でオブジェクトを定義する方法を書き、それから継承して、C++ オブジェクトと同じように拡張できるようにしました。基本的な考え方は次のとおりです。

  • 各オブジェクトには独自のファイルがありました
  • パブリック関数と変数は、オブジェクトの .h ファイルで定義されます
  • プライベート変数と関数は .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 ファイルは、各オブジェクトで何ができるべきかを正確に定義しました。

于 2009-01-06T04:28:55.403 に答える
34

C Object System (COS)は有望に思えます (まだアルファ版です)。シンプルさと柔軟性のために利用可能な概念を最小限に抑えようとします: オープン クラス、メタクラス、プロパティ メタクラス、ジェネリック、マルチメソッド、委任、所有権、例外、コントラクト、およびクロージャーを含む統一されたオブジェクト指向プログラミング。それを説明したドラフトペーパー(PDF)があります。

C の例外は、他のオブジェクト指向言語で見られる TRY-CATCH-FINALLY の C89 実装です。テストスイートといくつかの例が付属しています。

どちらもC の OOP に多くの作業を行っている Laurent Deniau によるものです。

于 2009-01-06T07:51:10.443 に答える
18

Linux 用の GNOME デスクトップはオブジェクト指向 C で書かれており、「GObject」と呼ばれるオブジェクト モデルを備えています。これは、プロパティ、継承、ポリモーフィズム、および参照、イベント処理 (「シグナル」と呼ばれる)、ランタイムなどの機能をサポートします。タイピング、個人データなど

これには、クラス階層内での型キャストなどを行うためのプリプロセッサ ハックが含まれています。GNOME 用に作成したクラスの例を次に示します (gchar のようなものは typedef です)。

クラス ソース

クラス ヘッダー

GObject 構造体の内部には、GLib の動的型付けシステムのマジック ナンバーとして使用される GType 整数があります (構造体全体を「GType」にキャストして型を見つけることができます)。

于 2009-01-06T05:06:40.650 に答える
7

少し話題から外れますが、元の C++ コンパイラであるCfrontは、C++ を C にコンパイルし、次にアセンブラにコンパイルしました。

ここに保存されます。

于 2009-08-05T09:55:58.153 に答える
6

オブジェクトで呼び出されるメソッドを、暗黙的な ' this' を関数に渡す静的メソッドと考えると、C での OO をより簡単に考えることができます。

例えば:

String s = "hi";
System.out.println(s.length());

になります:

string s = "hi";
printf(length(s)); // pass in s, as an implicit this

またはそのようなもの。

于 2009-01-06T05:04:55.380 に答える
6

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 でした。これを使用して、長さがプラットフォームごとに可変であることを思い出しました (移植のため)。

于 2009-01-06T05:14:39.540 に答える
5

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ファイル」を静的ライブラリまたは動的ライブラリにコンパイルできます。これによりカプセル化が作成され、実装を自由に変更することもできます。オブジェクトのユーザーは、オブジェクトの実装についてほとんど何も知る必要がありません。これにより、オブジェクトの全体的なデザインにも焦点が当てられます。

oop はコード構造と再利用性を概念化する方法であり、オーバーロードやテンプレートなどの c++ に追加される他のものとはまったく関係がないというのが私の個人的な信念です。はい、これらは非常に便利な機能ですが、オブジェクト指向プログラミングが実際に何であるかを代表するものではありません。

于 2015-01-08T16:04:38.777 に答える
4

ffmpeg (ビデオ処理用のツールキット) はストレート C (およびアセンブリ言語) で記述されていますが、オブジェクト指向スタイルを使用しています。関数ポインターを持つ構造体でいっぱいです。適切な「メソッド」ポインターで構造体を初期化する一連のファクトリー関数があります。

于 2009-01-06T04:31:34.980 に答える
3

私の推奨事項:シンプルに保ちます。私が抱えている最大の問題の 1 つは、古いソフトウェア (場合によっては 10 年以上前のもの) を維持することです。コードが単純でないと、難しい場合があります。はい、C でポリモーフィズムを使用して非常に便利な OOP を作成できますが、読むのは難しい場合があります。

私は、明確に定義された機能をカプセル化した単純なオブジェクトを好みます。これの良い例は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);

キーは次のとおりです。

  1. シンプルなアーキテクチャと設計パターン
  2. 基本的な OOP カプセル化を実現します。
  3. 実装、読み取り、理解、保守が容易
于 2013-10-10T19:59:19.917 に答える
1
#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

ここでは、C を使用したオブジェクト指向プログラミングとは何かを示します。

これは本物の純粋な C であり、プリプロセッサ マクロはありません。継承、ポリモーフィズム、およびデータのカプセル化 (クラスまたはオブジェクトにプライベートなデータを含む) があります。保護された修飾子と同等の可能性はありません。つまり、非公開データは継承チェーンでも非公開です。しかし、これは必要ないと思うので不便ではありません。

CPolygonインスタンス化されていません。これは、共通の側面を持ちながら実装が異なる継承チェーンのオブジェクトを操作するためにのみ使用するためです (ポリモーフィズム)。

于 2012-07-24T10:10:34.873 に答える
1

私が CI で OOP を書くつもりなら、おそらく疑似Pimpl設計を採用するでしょう。構造体へのポインターを渡す代わりに、構造体へのポインターへのポインターを渡すことになります。これにより、コンテンツが不透明になり、ポリモーフィズムと継承が容易になります。

C における OOP の本当の問題は、変数がスコープを出るときに何が起こるかということです。コンパイラによって生成されたデストラクタはなく、問題を引き起こす可能性があります。マクロが役立つ可能性はありますが、常に見苦しくなります。

于 2009-01-06T08:15:14.230 に答える
0

私にとって、Cのオブジェクト指向には次の機能が必要です。

  1. カプセル化とデータの非表示(構造体/不透明なポインターを使用して実現できます)

  2. 継承とポリモーフィズムのサポート(単一の継承は構造体を使用して実現できます-抽象ベースがインスタンス化できないことを確認してください)

  3. コンストラクタとデストラクタの機能(実現するのは簡単ではありません)

  4. 型チェック(少なくともCは強制しないため、ユーザー定義型の場合)

  5. 参照カウント(またはRAIIを実装するための何か)

  6. 例外処理の限定サポート(setjmpおよびlongjmp)

上記に加えて、ANSI / ISO仕様に依存する必要があり、コンパイラ固有の機能に依存するべきではありません。

于 2009-01-06T05:47:58.880 に答える
0

http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.htmlを見てください。ドキュメントを読むことは、他に何もないとしても、啓発的な経験です。

于 2016-08-08T12:39:35.183 に答える