web-dev-qa-db-ja.com

Cでクラスをどのように実装しますか?

C(C++またはオブジェクト指向コンパイラなし)を使用する必要があり、動的メモリ割り当てがないと仮定すると、クラスを実装するために使用できるいくつかのテクニック、またはクラスの適切な近似は何ですか? 「クラス」を別のファイルに分離することは常に良い考えですか?固定数のインスタンスを仮定するか、コンパイル時に各オブジェクトへの参照を定数として定義することにより、メモリを事前に割り当てることができると仮定します。どのOOPコンセプトを実装する必要があるかを自由に仮定し(変化します)、それぞれに最適な方法を提案してください。

制限事項:

  • 組み込みシステム用のコードを記述しているため、OOPではなくCを使用する必要があり、コンパイラと既存のコードベースはCにあります。
  • 動的なメモリ割り当ては、動的な割り当てを開始しても不足しないと合理的に想定できるほど十分なメモリがないため、ありません。
  • 使用するコンパイラーは、関数ポインターに問題はありません
130
Ben Gartner

これは、必要な正確な「オブジェクト指向」機能セットによって異なります。オーバーロードや仮想メソッドなどが必要な場合は、おそらく構造体に関数ポインターを含める必要があります。

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

これにより、基本クラスを「継承」して適切な関数を実装することにより、クラスを実装できます。

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

もちろん、これにはコンストラクターも実装する必要があります。コンストラクターは、関数ポインターが適切に設定されていることを確認します。通常、インスタンスに動的にメモリを割り当てますが、呼び出し元にもそれを許可できます。

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

いくつかの異なるコンストラクタが必要な場合は、関数名を「装飾」する必要があります。複数のrectangle_new()関数を使用することはできません。

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

使用法を示す基本的な例を次に示します。

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

少なくともこれでいくつかのアイデアが得られることを願っています。 Cで成功したリッチなオブジェクト指向フレームワークについては、glibの GObject ライブラリを調べてください。

また、上記でモデル化された明示的な「クラス」はなく、各オブジェクトには独自のメソッドポインターがあり、C++で通常見られるよりも少し柔軟であることに注意してください。また、メモリがかかります。メソッドポインターをclass構造に詰め込むことでそれを回避し、各オブジェクトインスタンスがクラスを参照する方法を考案できます。

81
unwind

宿題のためにも一度やらなければなりませんでした。私はこのアプローチに従いました:

  1. 構造体でデータメンバーを定義します。
  2. 構造体へのポインターを最初の引数として取る関数メンバーを定義します。
  3. これらを1つのヘッダーと1つのcで行います。構造体定義と関数宣言のヘッダー、実装のc。

簡単な例は次のとおりです。

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void Push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
23
erelender

1つのクラスのみが必要な場合は、structsの配列を「オブジェクト」データとして使用し、それらへのポインターを「メンバー」関数に渡します。 typedef struct _whatever Whateverを宣言する前にstruct _whateverを使用して、クライアントコードから実装を隠すことができます。このような「オブジェクト」とC標準ライブラリFILEオブジェクトの間に違いはありません。

継承関数と仮想関数を持つ複数のクラスが必要な場合、構造体のメンバーとして関数へのポインター、または仮想関数のテーブルへの共有ポインターを持つのが一般的です。 GObject ライブラリは、これとtypedefトリックの両方を使用し、広く使用されています。

このオンラインで利用可能なテクニックに関する本もあります- ANSI Cによるオブジェクト指向プログラミング

12
Pete Kirkham

Cインターフェイスと実装:再利用可能なソフトウェアを作成するためのテクニックデイビッド・R・ハンソン

http://www.informit.com/store/product.aspx?isbn=020149841

この本はあなたの質問をカバーする素晴らしい仕事をします。 Addison Wesley Professional Computingシリーズです。

基本的なパラダイムは次のようなものです。

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
7
Mark Harrison

gOBjectをご覧ください。これは、オブジェクトを実行するための冗長な方法を提供するOSライブラリです。

http://library.gnome.org/devel/gobject/stable/

7
Alex

structを使用して、クラスのデータメンバーをシミュレートします。メソッドスコープに関しては、.cファイルにprivate関数プロトタイプを配置し、.hファイルにpublic関数を配置することにより、プライベートメソッドをシミュレートできます。

4
Taylor Leese

CでのOOPの実行方法の簡単な例を示します。このシードは2009年のものであることがわかりましたが、とにかくこれを追加したいと思います。

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

基本的な概念には、構造の最上部に「継承クラス」を配置することが含まれます。このように、構造体の最初の4バイトにアクセスすると、「継承クラス」の最初の4バイトにもアクセスします(非クレイジーな最適化を想定)。これで、構造体のポインターが「継承クラス」にキャストされると、「継承クラス」は通常のメンバーにアクセスするのと同じ方法で「継承値」にアクセスできます。

コンストラクター、デストラクター、割り振り、および割り振り解除関数(これはinit、clean、new、freeをお勧めします)のこの命名規則といくつかの命名規則により、大きな道が開けます。

仮想関数については、おそらくClass_func(...)を使用して、構造体で関数ポインターを使用します。ラッパーも。 (単純な)テンプレートについては、size_tパラメーターを追加してサイズを決定するか、void *ポインターを必要とするか、必要な機能のみを備えた「クラス」型を必要とします。 (例:int GetUUID(Object * self); GetUUID(&p);)

4
YoYoYonnY
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};
4
Pierozi

あなたの場合、クラスの適切な近似は、an ADT です。しかし、それでも同じではありません。

3
Artem Barger

このオンラインブック は、問題の解決に役立つ可能性があります。

私の戦略は:

  • クラスのすべてのコードを別のファイルで定義する
  • 別のヘッダーファイルでクラスのすべてのインターフェイスを定義する
  • すべてのメンバー関数は、インスタンス名を表す「ClassHandle」を取ります(o.foo()の代わりに、foo(oHandle)を呼び出します)
  • コンストラクターは、メモリ割り当て戦略に応じて、関数void ClassInit(ClassHandle h、int x、int y、...)OR ClassHandle ClassInit(int x、int y、...)に置き換えられます
  • すべてのメンバー変数は、静的構造体のメンバーとしてクラスファイルに格納され、ファイルにカプセル化され、外部ファイルからのアクセスを防ぎます
  • オブジェクトは、上記の静的構造体の配列に格納され、定義済みのハンドル(インターフェイスで表示可能)またはインスタンス化できるオブジェクトの固定制限付き
  • 有用な場合、クラスには、配列をループしてインスタンス化されたすべてのオブジェクトの関数を呼び出すパブリック関数を含めることができます(RunAll()は各Run(oHandle)を呼び出します)
  • Deinit(ClassHandle h)関数は、動的割り当て戦略で割り当てられたメモリ(配列インデックス)を解放します

誰もこのアプローチのいずれかのバリエーションに問題、穴、潜在的な落とし穴、または隠れた利点/欠点を見ていますか?設計方法を再発明する場合(そして、そうする必要があると思います)、その名前を教えてもらえますか?

3
Ben Gartner

this answer および this one も参照してください

可能です。常に良いアイデアのように思えますが、その後はメンテナンスの悪夢になります。あなたのコードは、すべてを結び付けるコードの断片で散らかっています。新しいプログラマーは、関数ポインターを使用すると、どの関数が呼び出されるのかがわからないため、コードの読み取りと理解に多くの問題が発生します。

Get/set関数によるデータの非表示は、Cで簡単に実装できますが、そこで停止します。私はこれを組み込み環境で何度も試みてきましたが、結局は常にメンテナンスの問題です。

準備ができているすべてのメンテナンスの問題があるので、私は明確に操縦します。

3
Gerhard

このテーマに関する非常に広範な本があり、チェックする価値があります。

ANSI-Cのオブジェクト指向プログラミング

3
Ruben Steins

私のアプローチは、structおよびすべてのprimary-associated関数を別のソースファイルに移動して使用できるようにすることです。 「ポータブル」。

コンパイラに応じて、mightstructに関数を含めることができますが、それはveryコンパイラです固有の拡張子であり、私が日常的に使用している標準の最後のバージョンとは関係ありません。

2
warren

最初のc ++コンパイラは、実際にはC++コードをCに変換するプリプロセッサでした。

したがって、Cでクラスを作成することは非常に可能です。古いC++プリプロセッサを掘り下げて、それが作成するソリューションの種類を確認することができます。

2
Toad

GTKは完全にCで構築されており、多くのOOPコンセプトを使用しています。 GTKのソースコードを読みましたが、かなり印象的で、間違いなく読みやすくなっています。基本的な概念は、各「クラス」が単なる構造体であり、関連する静的関数であるということです。静的関数はすべて、パラメーターとして「インスタンス」構造体を受け入れ、必要な処理を行い、必要に応じて結果を返します。たとえば、「GetPosition(CircleStruct obj)」という関数があります。この関数は、単純に構造体を調べ、位置番号を抽出し、おそらく新しいPositionStructオブジェクトを作成し、新しいPositionStructにxとyを貼り付けて、それを返します。 GTKは、構造体の中に構造体を埋め込むことで、この方法で継承を実装します。とても賢い。

2
rocketsarefast

仮想メソッドが必要ですか?

そうでない場合は、構造体自体に関数ポインタのセットを定義するだけです。すべての関数ポインターを標準のC関数に割り当てると、C++の場合と非常によく似た構文でCから関数を呼び出すことができます。

仮想メソッドが必要な場合は、より複雑になります。基本的に、各構造体に独自のVTableを実装し、呼び出される関数に応じてVTableに関数ポインターを割り当てる必要があります。次に、構造体自体に関数ポインターのセットが必要になり、それがVTableの関数ポインターを呼び出します。これは、本質的に、C++が行うことです。

TBHただし...後者が必要な場合は、使用できるC++コンパイラを見つけてプロジェクトを再コンパイルすることをお勧めします。 C++が組み込みで使用できないという強迫観念を理解したことはありません。私は何度も使用しましたが、動作は速く、メモリの問題はありません。確かにあなたはあなたが何をするかについてもう少し注意する必要がありますが、本当にそれほど複雑ではありません。

1
Goz

CはOOP言語ではないので、あなたが正しく指摘しているように、真のクラスを作成する組み込みの方法はありません。 structs および function pointers を確認することをお勧めします。これらを使用すると、クラスの近似値を作成できます。ただし、Cは手続き型であるため、より多くのCライクなコードを記述することを検討することをお勧めします(つまり、クラスを使用しないでください)。

また、Cを使用できる場合は、おそらくC++を使用してクラスを取得できます。

0
user130076