web-dev-qa-db-ja.com

スタックとヒープのCで「クラス」を作成しますか?

Cの「クラス」(最初の引数としてそれへのポインタを受け取る関数にアクセスすることによって使用されることを意図した構造体)を見るときはいつでも、次のように実装されているのがわかります。

typedef struct
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_create();
void CClass_destroy(CClass *self);
void CClass_someFunction(CClass *self, ...);
...

この場合CClass_create常にmallocsそれはメモリであり、そのポインタを返します。

newがC++で不必要に表示されるのを見たときはいつでも、それは通常C++プログラマーを狂わせているように見えますが、この実践はCでは受け入れられるようです。ヒープに割り当てられた構造体「クラス」が非常に一般的である理由は何かありますか?

47
Therhang

これにはいくつかの理由があります。

  1. 「不透明な」ポインターの使用
  2. デストラクタの欠如
  3. 組み込みシステム(スタックオーバーフローの問題)
  4. コンテナ
  5. 慣性
  6. "怠惰"

それらについて簡単に説明しましょう。

不透明なポインターの場合、次のようなことができます。

struct CClass_;
typedef struct CClass_ CClass;
// the rest as in your example

そのため、ユーザーにはstruct CClass_の定義が表示されず、変更から隔離され、プラットフォームごとに異なる方法でクラスを実装するなど、その他の興味深い機能が有効になります。

もちろん、これはCClassのスタック変数の使用を禁止します。しかし、OTOH、これはCClassオブジェクトを静的に(プールから)割り当てることを禁止していないことがわかります-CClass_createまたは多分CClass_create_staticのような別の関数によって返されます。

デストラクタの欠如-CコンパイラはCClassスタックオブジェクトを自動的に破棄しないため、自分で行う必要があります(手動でデストラクタ関数を呼び出します)。したがって、残っている唯一の利点は、スタック割り当てが一般にヒープ割り当てよりも速いという事実です。 OTOH、ヒープを使用する必要はありません。プールやアリーナなどから割り当てることができ、スタックの割り当てと同じくらい高速で、以下で説明するスタックの割り当ての潜在的な問題がありません。

組み込みシステム-スタックは「無限」のリソースではありません。確かに、今日の「通常の」OS(POSIX、Windows ...)のほとんどのアプリケーションでは、ほぼそうです。ただし、組み込みシステムでは、スタックが数KBの低さになる場合があります。それは極端ですが、「大きな」組み込みシステムでも、MB単位のスタックがあります。そのため、使いすぎると使い果たしてしまいます。そうした場合、ほとんどの場合何が起こるかは保証されません-AFAIK、CとC++の両方で「未定義の動作」 OTOH、CClass_create()は、メモリが足りなくなったときにNULLポインターを返し、それを処理できます。

コンテナ-C++ユーザーはスタック割り当てが好きですが、スタックにstd::vectorを作成すると、その内容はヒープに割り当てられます。もちろん、それを微調整することもできますが、これはデフォルトの動作であり、そうでない場合の処理​​方法を理解するよりも、「コンテナのすべてのメンバーがヒープに割り当てられている」と言う方がはるかに簡単です。

慣性-さて、OOはSmalltalkからのものです。すべてがそこで動的であるため、Cへの「自然な」変換は「すべてをヒープに置く」方法です。したがって、最初の例はそのようであり、長年にわたって他の人に影響を与えました。

"Laziness"-スタックオブジェクトのみが必要であることがわかっている場合は、次のようなものが必要です。

CClass CClass_make();
void CClass_deinit(CClass *me);

ただし、スタックとヒープの両方を許可する場合は、以下を追加する必要があります。

CClass *CClass_create();
void CClass_destroy(CClass *me);

これは、実装者にとってやるべき仕事ですが、ユーザーを混乱させることにもなります。わずかに異なるインターフェイスを作成できますが、2つの関数セットが必要であるという事実は変わりません。

もちろん、「コンテナ」の理由は、部分的には「怠惰」の理由でもあります。

50

あなたの質問のように、CClass_createおよびCClass_destroy 使用する malloc/free、私にとって次のことは悪い習慣です:

void Myfunc()
{
  CClass* myinstance = CClass_create();
  ...

  CClass_destroy(myinstance);
}

mallocとfreeを簡単に回避できるためです。

void Myfunc()
{
  CClass myinstance;        // no malloc needed here, myinstance is on the stack
  CClass_Initialize(&myinstance);
  ...

  CClass_Uninitialize(&myinstance);
                            // no free needed here because myinstance is on the stack
}

CClass* CClass_create()
{
   CClass *self= malloc(sizeof(CClass));
   CClass_Initialize(self);
   return self;
}

void CClass_destroy(CClass *self);
{
   CClass_Uninitialize(self);
   free(self);
}

void CClass_Initialize(CClass *self)
{
   // initialize stuff
   ...
}

void CClass_Uninitialize(CClass *self);
{
   // uninitialize stuff
   ...
}

C++では、次のようにします。

void Myfunc()
{
  CClass myinstance;
  ...

}

これより:

void Myfunc()
{
  CClass* myinstance = new CCLass;
  ...

  delete myinstance;
}

不要なnew/deleteを回避するため。

14
Jabberwocky

Cでは、一部のコンポーネントが「作成」機能を提供する場合、コンポーネントの実装者もコンポーネントの初期化方法を制御します。したがって、エミュレートC++ 'operator newだけでなく、クラスコンストラクタ。

初期化に対するこの制御をあきらめると、入力のエラーチェックが大幅に増えるため、制御を維持することで、一貫性のある予測可能な動作を簡単に提供できます。

mallocalwaysがメモリの割り当てに使用されていることにも例外があります。これはよくあるケースですが、常にそうであるとは限りません。たとえば、一部の組み込みシステムでは、malloc/freeがまったく使用されていないことがわかります。 X_create関数は、他の方法で割り当てることができます。コンパイル時にサイズが固定されている配列から。

9

これは幾分意見ベースであるため、多くの答えを生み出します。それでも、なぜ私は自分の "Cオブジェクト"をヒープに割り当てたいのかを説明したいと思います。理由は、コードを消費することからすべてのフィールドを非表示にする(話す:private)ためです。これは不透明なポインタと呼ばれます。実際には、ヘッダーファイルは使用中のstructを定義せず、宣言するだけです。直接的な結果として、使用するコードはstructのサイズを認識できないため、スタックの割り当てが不可能になります。

利点は次のとおりです。コードを使用すると、structの定義に依存する決して依存することができません。つまり、struct外部からの不整合およびstructが変更されたときに、消費するコードの不要な再コンパイルを回避します。

最初の問題は、フィールドをprivateとして宣言することにより、 c ++ で対処されています。ただし、classのメンバーのみが変更された場合でも、privateの定義は、それを使用するすべてのコンパイルユニットにインポートされるため、再コンパイルする必要があります。 c ++ でよく使用される解決策はpimplパターンです:すべてのプライベートメンバーを2番目のstruct(または:class)に定義します実装ファイル内。もちろん、これにはpimplをヒープに割り当てる必要があります。

これに追加:modernOOP languages(eg Java or c# )など)呼び出し側のコードがオブジェクトの定義を知らなくても、オブジェクトを割り当てる(通常は内部的にスタックかヒープかを決定する)ことを意味します。

8
user2371524

「コンストラクタ」をvoid CClass_create(CClass*);に変更します

構造体のインスタンス/参照は返されませんが、呼び出されます。

「スタック」に割り当てられるか動的に割り当てられるかは、使用シナリオの要件に完全に依存します。どのように割り当てても、割り当てられた構造体をパラメータとして渡してCClass_create()を呼び出すだけです。

_{
    CClass stk;
    CClass_create(&stk);

    CClass *dyn = malloc(sizeof(CClass));
    CClass_create(dyn);

    CClass_destroy(&stk); // the local object lifetime ends here, dyn lives on
}

// and later, assuming you kept track of dyn
CClass_destroy(dyn); // destructed
free(dyn); // deleted
_

ローカル(スタックに割り当てられている)への参照を返さないように注意してください。これはUBであるためです。

どのように割り当てたとしても、正しい場所(そのオブジェクトの有効期間の終わり)でvoid CClass_destroy(CClass*);を呼び出す必要があり、動的に割り当てられた場合は、そのメモリも解放する必要があります。

割り当て/割り当て解除と構築/破壊を区別するこれらは同じではありません(C++の場合でも、自動的に結合される場合があります)。

3
dtech

一般に、_*_が表示されても、それがmallocであるとは限りません。たとえば、staticグローバル変数へのポインターを取得できます。あなたの場合、確かに、CClass_destroy()は、破棄されるオブジェクトに関するいくつかの情報をすでに知っていると仮定するパラメーターを取りません。

さらに、malloc 'dかどうかに関係なく、ポインターはオブジェクトを変更できる唯一の方法です。

スタックの代わりにヒープを使用する特定の理由はわかりません。メモリの使用量が減らないからです。ただし、そのような「クラス」を初期化するために必要なのはinit/destroy関数です。これは、基礎となるデータ構造に実際に動的データを含める必要があるため、ポインターを使用するためです。

3
edmz

Cには、C++プログラマーが当然のことと見なしている特定のものが欠けています。

  1. パブリックおよびプライベート指定子
  2. コンストラクタとデストラクタ

このアプローチの大きな利点は、Cファイルで構造体を非表示にして、作成関数と破棄関数を使用して正しい構築と破棄を強制できることです。

.hファイルで構造体を公開する場合、これは、ユーザーがメンバーに直接アクセスできることを意味し、カプセル化を解除します。また、作成を強制しないと、オブジェクトが正しく構築されなくなります。

2
doron

関数がスタックに割り当てられた構造体を返すことができるのは、他の割り当てられた構造体へのポインタが含まれていない場合のみです。単純なオブジェクト(int、bool、float、char、およびそれらの配列だけが含まれる場合no pointer)をスタックに割り当てることができます。ただし、返却するとコピーされることを知っておく必要があります。他の構造体へのポインタを許可したい場合、またはコピーを避けたい場合は、ヒープを使用してください。

しかし、トップレベルのユニットで構造体を作成し、呼び出された関数でのみ使用して、それを返さない場合は、スタックが適切です

2
Serge Ballesta

同時に存在する必要があるあるタイプのオブジェクトの最大数が固定されている場合、システムはすべての「ライブ」インスタンスで何かを実行できる必要があり、問題のアイテムはあまり多くのお金を消費しない、最高のアプローチは通常、ヒープ割り当てでもスタック割り当てでもありませんが、静的に割り当てられた配列と、「作成」および「破棄」メソッドがあります。配列を使用すると、オブジェクトのリンクリストを維持する必要がなくなり、「ビジー」であるためオブジェクトをすぐに破棄できない場合の処理​​が可能になります[e.g.データが割り込みまたはDMA=チャネル経由でチャネルに到着していないと判断したときにチャネル経由でデータが到着した場合、ユーザーコードは「dispose when done」フラグを設定して返すことができます。保留中の割り込みやDMA割り当てられなくなった上書きストレージ)について心配する必要はありません。

固定サイズのオブジェクトの固定サイズプールを使用すると、混合サイズのヒープからストレージを取得するよりも、割り当てと割り当て解除をはるかに予測しやすくなります。このアプローチは、需要が変動し、オブジェクトが(個別または集合的に)大量のスペースを占める場合には適切ではありませんが、需要がほぼ一貫している場合(たとえば、アプリケーションは常に12のオブジェクトを必要とし、時には最大3つのオブジェクトを必要とする場合)詳細)代替アプローチよりもはるかにうまく機能します。 1つの弱点は、静的バッファーが宣言されている場所でセットアップを実行するか、クライアントの実行可能コードで実行する必要があることです。クライアントサイトで変数初期化構文を使用する方法はありません。

ちなみに、このアプローチを使用する場合、クライアントコードに何かへのポインタを受信させる必要はありません。代わりに、都合のよいサイズの整数を使用してリソースを識別できます。さらに、リソースの数がintのビット数を超える必要がない場合は、一部のステータス変数でリソースごとに1ビットを使用すると役立つ場合があります。たとえば、変数timer_notifications(割り込みハンドラを介してのみ書き込まれる)とtimer_acks(メインラインコードを介してのみ書き込まれる)を設定し、(timer_notifications ^ timer_acks)のビットNがタイマーNの必要に応じて設定されるように指定できます。サービス。このようなアプローチを使用すると、コードは2つの変数を読み取るだけで、タイマーごとに1つの変数を読み取る必要がなく、タイマーがサービスを必要とするかどうかを判断できます。

2
supercat

あなたの質問は「なぜCではメモリを動的に割り当てるのが普通で、C++ではそうでないのか」ということですか。

C++には、新しい冗長性を実現する多くの構成要素が用意されています。コピー、移動、通常のコンストラクタ、デストラクタ、標準ライブラリ、アロケータ。

しかし、Cでは回避できません。

1

これは実際にはC++への反発であり、「新規」をあまりにも簡単にしています。

理論的には、このクラス構築パターンをCで使用することは、C++で「新規」を使用することと同じであるため、違いはありません。ただし、人々が言語について考える傾向が異なるため、人々がコードに反応する方法も異なります。

Cでは、コンピュータが目標を達成するために実行する必要がある正確な操作について考えることは非常に一般的です。それは普遍的ではありませんが、非常に一般的な考え方です。 malloc/freeのコスト/利益分析を実行するために時間がかかったと想定しています。

C++では、気付かないうちに多くのことを行うコード行を書くのがはるかに簡単になりました。誰かがコード行を書くことは非常に一般的であり、たまたま100または200の新規/削除が必要であることに気付かないことさえあります。これは反発を引き起こし、C++の開発者は、あちこちで誤って呼び出されているのではないかと心配して、ニュースや削除を狂ったようにひっくり返します。

もちろん、これらは一般化です。 CおよびC++コミュニティ全体がこれらの型に適合することは決してありません。ただし、ヒープ上に物を置くのではなく、newを使用することに失敗する場合は、これが根本的な原因である可能性があります。

1
Cort Ammon

よく目にするのは不思議ですね。あなたは「怠惰な」コードのように見えていたに違いありません。

Cでは、記述した手法は通常、「不透明な」ライブラリタイプ、つまり定義がクライアントのコードから意図的に非表示にされているstructタイプに予約されています。クライアントはそのようなオブジェクトを宣言できないため、イディオムは「非表示」のライブラリコードで動的割り当てを実際に行う必要があります。

構造体の定義を非表示にする必要がない場合、通常のCイディオムは通常次のようになります。

typedef struct CClass
{
    int member_a;
    float member_b;
} CClass;

CClass* CClass_init(CClass* cclass);
void CClass_release(CClass* cclass);

関数CClass_init*cclassオブジェクトを初期化し、結果と同じポインタを返します。つまりオブジェクトにメモリを割り当てる負担は呼び出し元にあり、呼び出し元は適切と思われる方法でそれを割り当てることができます

CClass cclass;
CClass_init(&cclass);
...
CClass_release(&cclass);

このイディオムの典型的な例は、pthread_mutex_tpthread_mutex_initおよびpthread_mutex_destroyです。

一方、前の手法を(元のコードのように)非不透明な型に使用することは、一般的に疑わしい手法です。これは、C++での動的メモリの不当な使用として正確に疑わしいものです。これは機能しますが、C++の場合と同様に、動的メモリを必要としない場合に使用することは、Cでも同じように嫌われます。

0
AnT