web-dev-qa-db-ja.com

ヒープで宣言されたオブジェクトがそのメンバーの1つをヒープで宣言することは意味がありますか?

私は最近、他の学生が書いたC++コードをレビューしましたが、彼は私が必要とは思わないようなことをしました。 main()で、彼はnew演算子を使用してヒープ上にクラスオブジェクトを作成します。次に、オブジェクトのメソッドを呼び出します。オブジェクトのメソッド自体が新しい演算子を呼び出して、大きなベクトルを作成します。

私の直感は、これは不要であると私に告げています。クラスオブジェクトはヒープに割り当てられているため、そのメンバーオブジェクトもヒープ上にあり、新しい余分なものが使用されています。私は正しいですか?

2
CMDoolittle

私が何か間違ったことを理解しない限り、いいえ、彼がしたことは正しいです。オブジェクトがヒープ上またはスタック上にあるという事実は、参照が属する場所とは何の関係もありません。たとえば、ヒープオブジェクトがスタックオブジェクトを参照していて、その逆も可能です。

最初のメソッドで2番目のオブジェクトをインスタンス化するためにnew演算子を使用しなかった場合、そのオブジェクトは他のスタックオブジェクトと同様にメソッドの戻り時にクリーンアップされていたはずです。

例:オブジェクトAにB * bフィールドがあります。コンストラクターでは、

{
    B tmpB = B()
    this->b = *tmpB
}

コンストラクターが終了するとすぐに、bフィールドは、有効なBオブジェクトではなくなったメモリーを指します。

ただし、B bフィールドがある場合は異なります。ここでbは値を保持し、参照ではなくなります。したがって、それはAと同じ場所になります(Aがスタック上にある場合はスタック、Aがnew経由で割り当てられている場合はヒープ)。

3
Arthur Havlicek

同様の質問が数週間前に行われました。

最も重要な認識は、私たちが話している2種類の「スタックとヒープ」があるということです。

  • データが実際に配置されている場所。つまり、配列の要素の1つへのポインタを取得した場合、このポインタはヒープに割り当てられたメモリ範囲の1つ、または実行スレッドのスタックメモリ範囲の1つに属しますか。
  • オブジェクトのスコープは何ですか-スコープバインド(スコープの終わりに到達するとオブジェクトが削除されるRAII、スコープベースのリソース管理を参照)、またはユーザー管理(言語は自動的にオブジェクトを解放しないため、ユーザーそれを解放するためにどこか他の場所でコードを書かなければなりません。)

スコープバインドについて説明する場合、スコープには次の2種類があります。

  • 関数呼び出し、または中括弧で囲まれたコードのブロック。
    • 関数(親)が別の関数(子)を呼び出すと、子関数が親のデータに対して何かを行わない限り、親のスコープで生きているすべてのものは子のスコープで生き続けます。
  • オブジェクトのインスタンスメンバー。
    • メンバーは、オブジェクトのコンストラクターが正常に終了すると有効になり、オブジェクトのデストラクタに入ると無効になります。

大きなベクトル

これが、この質問を特別なものにしているものです。大きなベクトルは、他の種類のプリミティブまたは集約とは異なる方法で処理されます。

  • 多くのプログラミングタスクでは、コンパイル時にベクトルのサイズを決定できません。
  • スレッドの呼び出しスタック(呼び出しフレーム)に予約されているメモリ(またはコミットされていない仮想アドレス空間)の量は、32ビット/ 64ビットのどちらでも制限されます(64ビットではほとんど問題にならない)。

C++で大きなベクトルを宣言するには、多くの方法があります。

int main(int argc, char** argv)
{
    // very likely located on stack, scope-bound
    int local_on_stack[1000];

    // very likely located on stack, scope-bound
    std::array<int, 1000uL> local_array;

    // allocated on heap, user-managed, need delete[]
    int* local_ptr_to_heap = new int[1000];

    // on heap, scope bound, thanks to std::vector<T>::~vector()
    std::vector<int> local_vec_of_int((size_t)1000, (int)0);

    // on heap, scope bound, thanks to std::unique_ptr<T[]>::~unique_ptr()
    std::unique_ptr<int[]> local_unique_array_of_int((size_t)1000);

    return 0;
}

配列サイズがコンパイル時定数でない場合、配列はヒープ上に作成される傾向があります。スタックに動的サイズの配列を作成できるようにする(必要に応じてスコープがバインドされている)コンパイラ固有の拡張機能があります。ヒープ、ただしメモリはスコープの終わりに達したときに解放されます)。

一つの観察をすることができます。コンパイラは、スコープの適切な出口点でその配列を解放するためのコードを密かに挿入する限り、ヒープに割り当てられた配列をスタックに割り当てられた配列に密かに置き換えることができます。

ただし、コンパイラは他の方向に置き換えることはできません。これは、スタック割り当てされている配列は、その作成者関数が終了すると無効になるためです。親関数が追加の子関数呼び出しを行うと、メモリ範囲は再利用され、呼び出しフレームとして再利用されます。

通常、以下は不要です。これは、(1)不要な場合にstd::vector<int>*deleteを呼び出す必要があるという余分な負担があり、(2)C++の新しいバージョンが複数の方法を提供するためです。 std::vectorの1つのインスタンスから別のインスタンスにデータの所有権を「転送」する方法。

std::vector<int>* pointer_to_vec_of_int = nullptr;
pointer_to_vec_of_int = new std::vector<int>(1000uL);
2
rwong

あなたの友人が間違ったことに正確に触れた答えはなかったので、私はこれを元に戻すと思いました。私は明らかにあなたの本能が何を選んだか正確に推測していますが、私はあなたに優れた本能を持ち、正確であることを認めます(Bravo!)。

ヒープに小さなクラスを割り当てることは、そのクラスがヒープにメモリを割り当てるだけの場合は少し無意味ですが、実際に問題なのはコードの重複です。友達のクラスは、メモリの割り当てと削除を担当します。新しいと宣言することで、あなたの友人はオブジェクトを削除する責任を負うので、メモリの割り当てを解除するので、オブジェクトが標準ライブラリベクトルを使用している場合は、実際にメモリ管理を2回コーディングする必要がありません。まったく(std :: vector自体は、ヒープにメモリを割り当てる小さなクラスです)。

Javaおよび類似の言語では、組み込み型ではないオブジェクトはすべて、新しいコマンドで作成する必要があります。多くの人がこの習慣をC++に引き継いでいますが、発見された、それは不必要であり、それはまた危険です。

新しいコマンドを直接使用することは、「高度な」C++と考える必要があります。本当に必要な場合以外は、行わないでください。新しいコマンドを使用するときは常に、対応する削除コマンドがあり、そのコマンドが常に1回だけ呼び出されることを確認する必要があります。これは難しい!絶対にやらないと言っているわけではありませんが、そうする必要はほとんどないはずです。そうする場合は、高度なことをしていることに注意し、特に注意する必要があります。おそらく標準ライブラリにそれを行う何かがあるでしょう。

これに対処するには2つの方法があります。「C++は怖い、Javaに戻る」と言うか、不要なときに新しいコマンドの使用をやめることができます。

C++にはガベージコレクションがないと人々が言うのを聞いたことがあるかもしれません。実際、C++には非常に高度な「ガベージコレクション」メカニズムがあります。それはそれをオフにすることが可能であるというだけです。

サウスパークの初期のエピソードをシミュレートする短いプログラムを次に示します。

#include<string>
#include<iostream>

using namespace std;

class Character{
  string my_name;
public:
  Character(string name) : my_name(name){
    cout << "Hi I'm " << my_name << " and I've just been born" << endl;
  }
  ~Character(){
    cout << "Oh my god, they killed " << my_name << "!" << endl;
  }
};
void south_park(){
  Character kyle("Kyle");
  Character stan("Stan");
  Character cartman("Cartman");

  cout << "Starting Episode" << endl;

  Character ike("Ike");

  {
    Character kenny("Kenny");
    cout << "Some Stuff Happens" << endl;
  }
  cout << "Some more stuff happens" << endl;
  cout << "Kyle learns something today" << endl;
  cout << "End of episode" << endl;

}

int main(int, char**){
  try{
    south_park();
    cout << "What a great episode of south park" << endl;
  }
  catch(...){
    cout << "Aww, we didn't finish the episode" << endl;
  }
}

プログラムを実行すると、Characterクラスが宣言されたときにすべての文字が生成されることがわかります。カイル、スタン、カートマンはすべてエピソードの開始前に生まれ、イケとケニーはエピソードの開始後に生まれましたが、ケニーは中括弧の中で生まれるのに十分残念です。

これは、変数Kennyが中括弧内のスコープにのみあることを意味します。中括弧の外で変数を参照しようとすると、エラーが発生します。ガベージコレクションされた言語では、オブジェクトはメモリが再び必要になるまで忘れられますが、C++ではオブジェクトがスコープから外れるとすぐにオブジェクトが破棄されます。キャラクターが死亡したことを伝えるために何もする必要はありません。クラスはデストラクタでそれ自体を処理します。コードをいじって、さまざまな場所でキャラクターを初期化したときにキャラクターがどうなるかを確認してください。

生まれたすべてのキャラクターは常に死ぬことに注意してください。また、キャラクターは作成されたときとは逆の順序で死ぬことに注意してください。これは自動です!エピソードのどこかにreturnステートメントまたはthrowを入れてみてください。どこに置いてもかまいません。一部の文字は生まれない場合がありますが、関数が戻る前に、生まれたすべての文字が死んでしまいます。これは、c ++での「ガベージコレクション」(リソース管理と呼びます)の鍵です。

関数のどこかで宣言した場合

Character* butters = new Character("Butters")

その後、私が明示的に削除バターを呼び出さない限り、バターはエピソードの最後でも存続します。これはc ++では悪いことです。

C++では、新しいコマンドを使用するとき、コンパイラーに「私がやっていることを知っているので、ガベージコレクションをオフにしてください」と効果的に言っていますが、多くの場合、実際には「ガベージコレクションをオンにしました」と言っています。何をしているか分からないからです」.

C++でメモリを割り当てるか、ファイルを開くか、後でクリーンアップする必要があることを行うときは、常にそれをクラスでラップする必要があります。これは、デストラクタで行う必要があるすべてのクリーンアップを行い、そのオブジェクトは常に次のいずれかである必要があります。スタックまたはクラスのメンバー変数にある。 (クラスが破棄されると、メンバー変数は適切に破棄されます)馬鹿がnewでクラスを宣言し、それを削除しない場合、それは彼の問題ではありません。

したがって、私のサウスパークプログラムでは、私のキャラクターの1人がメモリを割り当てる必要がある場合、デストラクタが正確に1回呼び出されることを十分に理解しているため、クラスが追跡していることを確認する限り、安全に削除を呼び出すことができます。それが割り当てたリソースの実際、それはあなたがそれに気づくことさえなくてもすでにそれを行っています。

文字列クラスは32バイトです(64ビットLinuxのclangの場合)。文字列が32バイト内に収まるほど短い場合は、そこに格納されます。文字列が長い場合は、ヒープに文字列を割り当てます。文字列の長さを変更すると、新しいメモリが割り当てられ、文字列がコピーされて古いメモリが削除されます。デストラクタでは、割り当てたメモリを削除します。このすべては、あなたがそれが起こったことを知らなくても対処されます。文字列がスタックにある限り、そのメモリは常に正しく割り当て解除されます。あなたが新しいものを使うとすぐに、あなたはあなた自身にいます。

その原則全体は、「リソースの取得は初期化」またはRAIIと呼ばれます。誰もが同意するものはひどい名前なので、最後までそれを呼ばなかったのはそのためです。スタックオーバーフローに関する記事はたくさんあります。詳しく知りたい場合は、記事や講演などをご覧ください。これを正しく行うことは、良いc ++と悪いc ++の違いです。

1
Tim

ヒープ上のオブジェクトでさえ、割り当てられると成長できません

より多くのスペース(集約オブジェクト/ノード/バイト)を割り当て、それを既存のオブジェクトにリンクすることのみが可能です。

sizeof() anyの特定のオブジェクトは定数であり、構成が定義されているクラスによって定義されます。

質問の前提は一般的なものであり、スニペット(およびそれを最適化する方法)に固有のものではないため、ピアの実装アプローチに問題はありません。

STLコンテナー(ベクター、リスト、セット、マップ、両端キュー)を検討します。これらはすべて、動的メモリを使用して、つまりヒープ上で動作する必要があります。したがって、これらのインスタンスをヒープ上に作成する場合、それはあなたの質問と同じです。ただし、要件がコンテナをヒープ上に置くように指示している場合は、そうすることは間違いなく正しいでしょう。

たとえば、class MailBoxが内部にstd::list of MailFolderオブジェクトを持ち、各MailFolderstd::list of Mailオブジェクト。

    class Mail
    {
    ...
    };

    class MailFolder
    {
    private:
        std::list<Mail> mReceivedMail;
    ...
    };

    class MailBox
    {
    private:
        std::list<MailFolder> mFolders;
    ...
    };

    Mailbox mb;
    //'mb' object is on the stack and not the heap.

この場合、ヒープに何も配置していないように見えます。

std::listは動的メモリを使用し、MailBox::mFoldersの内容をヒープに配置します。したがって、すべてのMailFolderオブジェクトはヒープ上にあります。次に、MailFolder::mReceivedMailの内容は、MailFolderオブジェクトのメモリ空間の外で、ヒープに個別に再度配置されます。この設計に必要なのは、ユーザーがメールボックスに作成するフォルダーの数も、各フォルダーで受信するメールの数も事前に決定できないことです。

要約すると、これは完全に有効なシナリオであり、ヒープメモリ自体に既にあるオブジェクトの内部からヒープメモリを割り当てます。

これが理解を曇らせるよりも疑いを明確にすることを願っています。 :-)

0
Ramakant