Cでクラスをどのように実装しますか?[閉まっている]


139

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

制限:

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

26
必須の質問:オブジェクト指向のコードを記述する必要がありますか?なんらかの理由でやれば大丈夫ですが、かなり難しい戦いになります。Cでオブジェクト指向のコードを記述しないようにすると、おそらく最良の結果が得られます。確かに可能です-unwindの優れた答えを参照してください-しかし、それは正確に「簡単」ではなく、メモリが限られた組み込みシステムで作業している場合は、実現できない場合があります。しかし、私は間違っているかもしれません-私はそれについてあなたを主張しようとはしていません。
Chris Lutz、

1
厳密に言えば、私たちはそうする必要はありません。ただし、システムが複雑なため、コードを保守できなくなっています。複雑さを軽減する最善の方法は、いくつかのOOP概念を実装することだと私は感じています。3分以内に回答してくださった皆様、ありがとうございました。君たちはめちゃくちゃ速くて!
ベンガートナー、

8
これは私の控えめな意見ですが、OOPはコードを即座に保守可能にしません。これにより、管理が容易になる場合がありますが、必ずしも保守しやすくなるとは限りません。Cで「名前空間」(Apache Portable Runtimeはすべてのグローバルシンボルにapr_接頭辞を付け、GLibに接頭辞を付けてg_名前空間を作成します)およびOOPのない他の構成要素を持つことができます。とにかくアプリを再構築するつもりなら、もっとメンテナンスしやすい手続き構造を考え出すために少し時間を費やすことを検討します。
Chris Lutz、

これは以前から延々と議論されてきました-あなたは以前の答えのいずれかを見ましたか?
ラリーワタナベ

:鉱山の削除答えていた、また助けになることがあり、このソース、planetpdf.com/codecuts/pdfs/ooc.pdfこれは、Cでオブジェクト指向を行うための完全なアプローチを説明
ルーベンシュタインズ・

回答:


86

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

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構造体に詰め込むことでそれを回避し、各オブジェクトインスタンスがクラスを参照する方法を発明できます。


オブジェクト指向のCを作成する必要がなかったので、引数を取る、const ShapeClass *またはconst void *引数として受け取る関数を作成するのが通常最善でしょうか?後者の方が継承については少し優れているように思われるかもしれませんが、私は議論を両方の方法で見ることができます...
Chris Lutz

1
@クリス:ええ、それは難しい質問です。:| GTK +(GObjectを使用)は、適切なクラス、つまりRectangleClass *を使用します。これは、キャストを行う必要があることが多いことを意味しますが、便利なマクロがそれを支援するため、SUBCLASS(p)だけを使用してBASECLASS * pをSUBCLASS *にキャストできます。
アンワインド

1
私のコンパイラはコードの2行目で失敗します。float (*computeArea)(const ShapeClass *shape);それShapeClassは不明なタイプであると言っています。
DanielSank 2016

@DanielSankは、 'typedef struct`に必要な前方宣言がないためです(上記の例には示されていません)。struct参照自体なので、定義する宣言が必要です。これは、ここLundinの回答の例で説明されています。例を変更して前方宣言を含めると、問題が解決するはずです。typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S.ウィテカー2017年

Rectangleに、すべてのShapeが実行できるわけではない機能がある場合にどうなりますか。たとえば、get_corners()などです。円はこれを実装しませんが、長方形は実装する可能性があります。継承元の親クラスの一部ではない関数にどのようにアクセスしますか?
Otus

24

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

  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.
/// 

これは私が過去にやったことですが、関数プロトタイプを必要に応じて.cまたは.hファイルに配置することにより、偽のスコープが追加されています(私の回答で述べたように)。
テイラーリース

私はこれが好きです、構造体宣言はすべてのメモリを割り当てます。何らかの理由で、これがうまくいくことを忘れていました。
ベンガートナー、

あなたtypedef struct Queue Queue;はそこに必要だと思います。
Craig McQueen

3
または単にtypedef struct {/ * members * /} Queue;
Brooks Moses

#Craig:リマインダーをありがとう、更新しました。
erelender 2009

12

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

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

このオンラインで利用可能な手法に関する本(ANSI Cによるオブジェクト指向プログラミング)もあります。


1
涼しい!CでのOOPに関する本の他の推奨事項はありますか?または、Cの他のモダンデザインテクニックはありますか?(または組み込みシステム?)
Ben Gartner

7

GOBjectをご覧ください。これは、オブジェクトを実行する詳細な方法を提供するOSライブラリです。

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


1
興味深い。ライセンスについて誰もが知っていますか?私の仕事の目的では、オープンソースライブラリをプロジェクトにドロップすることは、おそらく法的な観点からは機能しません。
ベンガートナー、

GTK +、およびそのプロジェクト(GObjectを含む)の一部であるすべてのライブラリは、GNU LGPLの下でライセンスされます。つまり、独自のソフトウェアからそれらにリンクできます。しかし、それが組み込み作業で実現可能かどうかはわかりません。
Chris Lutz、

7

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

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

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

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

/* for data structure foo */

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

5

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);)


免責事項:スマートフォンで書かれたすべてのコード。必要に応じてエラーチェックを追加します。バグをチェックします。
yyny

4

a structを使用して、クラスのデータメンバーをシミュレートします。メソッドスコープの観点から、プライベート関数のプロトタイプを.cファイルに配置し、パブリック関数を.hファイルに配置することで、プライベートメソッドをシミュレートできます。


4
#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;
};

3

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


1
誰かが抽象データ型とクラスの簡単な違いを与えることはできますか?私は常に2つの概念を密接に関連しています。
ベンガートナー、

それらは確かに密接に関連しています。クラスは、(おそらく)同じインターフェースを満たす別の実装に置き換えることができるため、ADTの実装と見なすことができます。ただし、コンセプトが明確に定義されていないため、正確な違いを示すのは難しいと思います。
ヨルゲンFogh

3

私の戦略は:

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

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


スタイルの問題として、質問に追加する情報がある場合は、質問を編集してこの情報を含める必要があります。
Chris Lutz、

別のオブジェクトを要求して1つを提供するリソースがない場合に何が起こるかについて実際に何かを行うのではなく、大きなヒープから動的に割り当てるmallocから固定サイズのプールから動的に選択するClassInit()に移動したようです。
ピートKirkham

はい、メモリ管理の負担はClassInit()を呼び出すコードに移され、返されたハンドルが有効であることを確認します。基本的に、クラス専用の独自のヒープを作成しました。汎用ヒープを実装しない限り、動的割り当てを行いたい場合にこれを回避する方法はわかりません。ヒープで継承されるリスクを1つのクラスに分離することをお勧めします。
ベンガートナー、

3

参照してください、この答えをし、これ

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

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

皆さんは準備ができているので、メンテナンスの問題があります。


2

私のアプローチは、structとすべての主に関連付けられた関数を別のソースファイルに移動して、「ポータブル」に使用できるようにすることです。

コンパイラによっては、に関数を含めることができる場合ありますstruct、これは非常にコンパイラ固有の拡張機能であり、私が日常的に使用している標準の最後のバージョンとは関係ありません:)


2
関数ポインタはすべて良いです。これらを使用して、大きなswitchステートメントをルックアップテーブルに置き換える傾向があります。
ベンガートナー、

2

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

したがって、Cでクラスを作成することは非常に可能です。古いC ++プリプロセッサを試してみて、それがどのようなソリューションを作成するかを確認することができます。


それはそうでしょうcfront。例外がC ++に追加されたときに問題が発生しました-例外の処理は簡単ではありません。
ジョナサンレフラー

2

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


1

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

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

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

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


私はすでにそれを言ったので、もう一度言いますが、もう一度言います。CでOOPを作成するために、関数ポインターや構造体C ++スタイルから関数を呼び出す機能は必要ありません。OOPは、主に機能と変数の継承に関するものです。 (コンテンツ)どちらも、関数ポインターや重複コードなしでCで実現できます。
yyny

0

CはOOP言語ではないため、ご指摘のとおり、真のクラスを作成する組み込みの方法はありません。あなたは最善の策は、構造体、および関数ポインタを調べることです、これらはあなたがクラスの近似を構築することを可能にします。ただし、Cは手続き型であるため、より多くのCに似たコードを(つまり、クラスを使用せずに)作成することを検討できます。

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


4
私は反対票を投じませんが、FYI、関数ポインタ、または構造体から関数を呼び出す機能(あなたの意図ではあると思います)はOOPとは関係ありません。OOPは主に機能と変数の継承に関するもので、どちらもCで関数ポインターや重複なしに実現できます。
yyny
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.