75

いくつかのクラスを定義するとします。

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

次に、それを使用していくつかのコードを記述します。なぜ私は次のことをするのですか?

Pixel p;
p.x = 2;
p.y = 5;

Java の世界から来て、私はいつも次のように書いています。

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

彼らは基本的に同じことをしますよね?1 つはスタック上にあり、もう 1 つはヒープ上にあるため、後で削除する必要があります。両者の間に根本的な違いはありますか?どちらかを優先する必要があるのはなぜですか?

4

23 に答える 23

188

はい、1 つはスタック上にあり、もう 1 つはヒープ上にあります。2 つの重要な違いがあります。

  • まず、明白で重要度の低いもの: ヒープの割り当てが遅い。スタック割り当ては高速です。
  • 2 番目に重要なのはRAIIです。スタック割り当てバージョンは自動的にクリーンアップされるため、便利です。そのデストラクタは自動的に呼び出され、クラスによって割り当てられたすべてのリソースがクリーンアップされることを保証できます。これは、C++ でメモリ リークを回避する基本的な方法です。代わりに、通常はデストラクタで内部的にdelete呼び出すスタック割り当てオブジェクトにラップすることで、それらを回避します。deleteすべての割り当てを手動で追跡deleteし、適切なタイミングで呼び出そうとすると、コード 100 行ごとに少なくとも 1 回のメモリ リークが発生することが保証されます。

小さな例として、次のコードを検討してください。

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

かなり無害なコードですよね?ピクセルを作成し、無関係な関数を呼び出してから、ピクセルを削除します。メモリリークはありますか?

そして答えは「おそらく」です。bar例外をスローするとどうなりますか? delete呼び出されることはなく、ピクセルは削除されず、メモリ リークが発生します。これを考慮してください:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

これはメモリをリークしません。もちろん、この単純なケースでは、すべてがスタック上にあるため、自動的にクリーンアップされますが、Pixelクラスが内部で動的割り当てを行ったとしても、それもリークしません。クラスには、Pixelそれを削除するデストラクタが与えられるだけで、このデストラクタは、関数をどのように終了しても呼び出されfooます。bar例外をスローしたのでそのままにしておいても。次の少し不自然な例は、これを示しています。

class Pixel {
public:
  Pixel(){ x=new int(0); y=new int(0);};
  int* x;
  int* y;

  ~Pixel() {
    delete x;
    delete y;
  }
};

void foo() {
  Pixel p;
  *p.x = 2;
  *p.y = 5;

  bar();
}

Pixel クラスはヒープ メモリを内部的に割り当てるようになりましたが、そのデストラクタがクリーンアップを処理するため、クラスを使用するときは心配する必要はありません。(ここでの最後の例は、一般的な原則を示すために大幅に単純化されていることを言及しておく必要があります。実際にこのクラスを使用すると、いくつかのエラーが含まれる可能性があります。y の割り当てが失敗した場合、x は決して解放されません。 、Pixel がコピーされた場合、両方のインスタンスが同じデータを削除しようとすることになります。したがって、ここで最後の例を見てみましょう。実際のコードは少しトリッキーですが、一般的な考え方を示しています)。

もちろん、同じ手法をメモリ割り当て以外のリソースに拡張することもできます。たとえば、使用後にファイルまたはデータベース接続が閉じられること、またはスレッド コードの同期ロックが解放されることを保証するために使用できます。

于 2009-06-30T15:34:14.730 に答える
30

削除を追加するまで、これらは同じではありません。
あなたの例は非常に些細なものですが、デストラクタには実際に実際の作業を行うコードが含まれている可能性があります。これはRAIIと呼ばれます。

したがって、削除を追加します。例外が伝播している場合でも、それが発生することを確認してください。

Pixel* p = NULL; // Must do this. Otherwise new may throw and then
                 // you would be attempting to delete an invalid pointer.
try
{
    p = new Pixel(); 
    p->x = 2;
    p->y = 5;

    // Do Work
    delete p;
}
catch(...)
{
    delete p;
    throw;
}

ファイル(閉じる必要のあるリソース)のようなもっと面白いものを選んだ場合。次に、これを行うために必要なポインタを使用して、Javaで正しく実行します。

File file;
try
{
    file = new File("Plop");
    // Do work with file.
}
finally
{
    try
    {
        file.close();     // Make sure the file handle is closed.
                          // Oherwise the resource will be leaked until
                          // eventual Garbage collection.
    }
    catch(Exception e) {};// Need the extra try catch to catch and discard
                          // Irrelevant exceptions. 

    // Note it is bad practice to allow exceptions to escape a finally block.
    // If they do and there is already an exception propagating you loose the
    // the original exception, which probably has more relevant information
    // about the problem.
}

C++の同じコード

std::fstream  file("Plop");
// Do work with file.

// Destructor automatically closes file and discards irrelevant exceptions.

人々は速度について言及していますが(ヒープ上のメモリを見つけて割り当てるため)。個人的には、これは私にとって決定的な要因ではありません(アロケータは非常に高速で、絶えず作成/破棄される小さなオブジェクトのC ++使用に最適化されています)。

私の主な理由は、オブジェクトの寿命です。ローカルで定義されたオブジェクトには、非常に具体的で明確に定義された存続期間があり、デストラクタは最後に呼び出されることが保証されています(したがって、特定の副作用が発生する可能性があります)。一方、ポインタは動的な寿命を持つリソースを制御します。

C++とJavaの主な違いは次のとおりです。

誰がポインタを所有するかという概念。適切なタイミングでオブジェクトを削除するのは所有者の責任です。これが、実際のプログラムでそのような生のポインターが表示されることはめったにない理由です(生のポインターに関連付けられた所有権情報がないため)。代わりに、ポインターは通常、スマートポインターでラップされます。スマートポインタは、誰がメモリを所有し、誰がメモリをクリーンアップする責任があるかというセマンティクスを定義します。

例は次のとおりです。

 std::auto_ptr<Pixel>   p(new Pixel);
 // An auto_ptr has move semantics.
 // When you pass an auto_ptr to a method you are saying here take this. You own it.
 // Delete it when you are finished. If the receiver takes ownership it usually saves
 // it in another auto_ptr and the destructor does the actual dirty work of the delete.
 // If the receiver does not take ownership it is usually deleted.

 std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr
 // A shared ptr has shared ownership.
 // This means it can have multiple owners each using the object simultaneously.
 // As each owner finished with it the shared_ptr decrements the ref count and 
 // when it reaches zero the objects is destroyed.

 boost::scoped_ptr<Pixel>  p(new Pixel);
 // Makes it act like a normal stack variable.
 // Ownership is not transferable.

他にもあります。

于 2009-06-30T15:50:12.457 に答える
25

論理的には、クリーンアップを除いて、同じことを行います。あなたが書いたサンプルコードだけでは、メモリが解放されていないため、ポインタの場合にメモリリークがあります。

Java のバックグラウンドを持っているため、何が割り当てられ、誰がそれを解放する責任があるかを追跡することを中心に C++ がどの程度展開されているかについて、完全に準備ができていない可能性があります。

必要に応じてスタック変数を使用することで、その変数を解放することを心配する必要がなくなります。スタック フレームがなくなります。

明らかに、細心の注意を払っていれば、いつでも手動でヒープに割り当てて解放することができますが、優れたソフトウェア エンジニアリングの一部は、超人的なプログラマーを信頼するのではなく、壊れないように構築することです。絶対に間違えないように。

于 2009-06-30T15:33:32.097 に答える
24

次の理由から、機会があれば常に最初の方法を使用することを好みます。

  • それはより速いです
  • メモリの解放について心配する必要はありません
  • p は、現在のスコープ全体で有効なオブジェクトになります
于 2009-06-30T15:32:53.547 に答える
14

「C++ のすべてにポインターを使用しない理由」

簡単な答えの 1 つ - メモリの管理が大きな問題になるため、割り当てと削除/解放です。

自動/スタック オブジェクトは、その忙しい作業の一部を取り除きます。

それは私が質問について最初に言うことです。

于 2009-06-30T15:33:03.017 に答える
11

コード:

Pixel p;
p.x = 2;
p.y = 5;

メモリの動的割り当てはありません-空きメモリの検索も、メモリ使用量の更新も何もありません。それは完全に無料です。コンパイラは、コンパイル時に変数用にスタックにスペースを予約します。予約するスペースが十分にあることがわかり、スタック ポインターを必要な量だけ移動するための単一のオペコードを作成します。

new を使用すると、メモリ管理のオーバーヘッドがすべて必要になります。

問題は、データにスタック領域を使用するかヒープ領域を使用するかということです。'p' のようなスタック (またはローカル) 変数は逆参照を必要としませんが、new を使用すると間接的なレイヤーが追加されます。

于 2009-06-30T15:34:07.913 に答える
11

一般的な経験則として、どうしても必要な場合を除き、決して new を使用しないでください。new を使用しないと、クリーンアップする場所を気にする必要がないため、プログラムの保守が容易になり、エラーが発生しにくくなります。

于 2009-06-30T15:35:25.820 に答える
10

はい、Java または C# のバックグラウンドがあるため、最初はそれが理にかなっています。割り当てたメモリを解放することを覚えておく必要があるのは大したことではないようです。しかし、最初のメモリ リークが発生すると、すべてを解放したと誓ったため、頭を悩ませることになります。それが 2 回目、3 回目になると、さらにイライラすることになります。最後に、メモリの問題による頭痛の種が 6 か月続くと、それに飽き始め、スタックに割り当てられたメモリがますます魅力的に見えるようになります。なんてすてきできれいなのでしょう。スタックに置いて忘れてください。すぐにスタックを使いこなせるようになるでしょう。

しかし、その経験に代わるものはありません。私のアドバイス?今のところ、自分のやり方で試してみてください。わかるでしょ。

于 2009-06-30T15:45:35.737 に答える
6

私の直観的な反応は、これが重大なメモリ リークにつながる可能性があることです。ポインターを使用している状況によっては、ポインターを削除する責任を誰が負うべきかについて混乱が生じる可能性があります。あなたの例のような単純なケースでは、いつどこで削除を呼び出す必要があるかを簡単に確認できますが、クラス間でポインターを渡し始めると、少し難しくなる可能性があります。

ポインターのブーストスマート ポインター ライブラリを調べることをお勧めします。

于 2009-06-30T15:31:43.440 に答える
6

すべてを新しくしない一番の理由は、物事がスタック上にあるときに非常に決定論的なクリーンアップができるからです。Pixel の場合、これはそれほど明白ではありませんが、たとえばファイルの場合、これは有利になります。

  {   // block of code that uses file
      File aFile("file.txt");
      ...
  }    // File destructor fires when file goes out of scope, closing the file
  aFile // can't access outside of scope (compiler error)

ファイルを新規作成する場合は、同じ動作を得るためにファイルを削除することを忘れないでください。上記の場合、単純な問題のようです。ただし、ポインターをデータ構造に格納するなど、より複雑なコードを検討してください。そのデータ構造を別のコードに渡すとどうなるでしょうか? クリーンアップの責任者。あなたのすべてのファイルを閉じるのは誰ですか?

すべてを新しくしない場合、変数がスコープ外になると、リソースはデストラクタによってクリーンアップされます。そのため、リソースが正常にクリーンアップされたことを確信できます。

この概念は RAII (リソース割り当ては初期化) として知られており、リソースの取得と破棄を処理する能力を大幅に向上させることができます。

于 2009-06-30T15:32:59.647 に答える
6

最初のケースは、常にスタックが割り当てられるわけではありません。オブジェクトの一部である場合は、オブジェクトがある場所に割り当てられます。例えば:

class Rectangle {
    Pixel top_left;
    Pixel bottom_right;
}

Rectangle r1; // Pixel is allocated on the stack
Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap

スタック変数の主な利点は次のとおりです。

  • RAII パターンを使用してオブジェクトを管理できます。オブジェクトが範囲外になるとすぐに、そのデストラクタが呼び出されます。C# の "using" パターンに似ていますが、自動です。
  • null 参照の可能性はありません。
  • オブジェクトのメモリを手動で管理する必要はありません。
  • これにより、メモリ割り当てが少なくなります。C++ では、Java よりもメモリ割り当てが遅くなる可能性があります。

オブジェクトが作成されると、ヒープに割り当てられたオブジェクトとスタック (またはどこにでも) に割り当てられたオブジェクトの間にパフォーマンスの違いはありません。

ただし、ポインターを使用しない限り、いかなる種類のポリモーフィズムも使用できません。オブジェクトには、コンパイル時に決定される完全に静的な型があります。

于 2009-06-30T15:38:07.470 に答える
4

好みの問題が大きいと思います。メソッドが参照の代わりにポインターを受け取ることを許可するインターフェイスを作成すると、呼び出し元が nil を渡すことが許可されます。ユーザーが nil を渡すことを許可しているため、ユーザーは nil を渡します。

「このパラメーターが nil の場合はどうなるか」を自問する必要があるため、null チェックを常に処理して、より防御的にコーディングする必要があります。これは、参照を使用することを意味します。

ただし、nil を渡すことができるようにしたい場合は、参照は問題外です:) ポインターを使用すると、柔軟性が向上し、より怠惰になることができます。これは本当に良いことです。割り当てなければならないことがわかるまで、決して割り当てないでください。

于 2009-06-30T15:33:03.750 に答える
4

オブジェクトの有効期間。オブジェクトの有効期間を現在のスコープの有効期間よりも長くしたい場合は、ヒープを使用する必要があります。

一方、現在のスコープを超えて変数が必要ない場合は、スタックで宣言します。範囲外になると自動的に破棄されます。アドレスの受け渡しには注意してください。

于 2009-06-30T15:33:57.197 に答える
4

問題はポインター自体ではなく(ポインターの導入は別としてNULL)、手動でメモリ管理を行うことです。

もちろん、おもしろいのは、私が見たすべての Java チュートリアルで、ガベージ コレクターは非常にクールであると述べられていることdeleteです。)。deletenewdelete[]new[]

于 2009-06-30T18:39:48.260 に答える
2

すべてにポインターを使用しないのはなぜですか?

彼らは遅いです。

コンパイラーの最適化は、ポインター アクセス シンタックスではそれほど効果的ではありません。これについては、さまざまな Web サイトで読むことができますが、Intel からの適切な pdf を次に示します。

ページ、13、14、17、28、32、36 を確認してください。

ループ表記での不要なメモリ参照の検出:

for (i = j + 1; i <= *n; ++i) { 
X(i) -= temp * AP(k); } 

ループ境界の表記には、ポインターまたはメモリー参照が含まれます。コンパイラーには、ポインター n によって参照される値が、他の代入によるループ反復で変更されているかどうかを予測する手段がありません。これはループを使用して、反復ごとに n によって参照される値を再読み込みします。コード ジェネレーター エンジンは、潜在的なポインター エイリアシングが見つかった場合、ソフトウェア パイプライン ループのスケジューリングも拒否する場合があります。ポインター n によって参照される値はループ内で変化せず、ループ インデックスに対して不変であるため、*ns の読み込みは、より簡単なスケジューリングとポインターの明確化のためにループ境界の外で実行されます。

...このテーマのバリエーションの数....

複雑なメモリ参照。つまり、複雑なポインター計算などの参照を分析すると、コンパイラーが効率的なコードを生成する能力に負担がかかります。データが存在する場所を特定するために、コンパイラまたはハードウェアが複雑な計算を実行しているコード内の場所に注目する必要があります。ポインターのエイリアシングとコードの単純化は、コンパイラーがメモリ アクセス パターンを認識するのを支援し、コンパイラーがメモリ アクセスとデータ操作をオーバーラップできるようにします。不要なメモリ参照を減らすと、コンパイラがソフトウェアをパイプライン処理できるようになる場合があります。エイリアシングやアラインメントなどの他の多くのデータ位置プロパティは、メモリ参照計算が単純に保たれていれば簡単に認識できます。

于 2009-07-14T06:24:07.150 に答える
2

ポインターと動的に割り当てられたオブジェクトは、必要な場合にのみ使用してください。可能な限り、静的に割り当てられた (グローバルまたはスタック) オブジェクトを使用します。

  • 静的オブジェクトはより高速です (新規作成/削除なし、それらにアクセスするための間接化なし)
  • 心配するオブジェクトの有効期間はありません
  • より少ないキーストローク
  • はるかに堅牢です。すべての「->」は、NIL または無効なメモリへの潜在的なアクセスです

明確にするために、このコンテキストで「静的」とは、動的に割り当てられていないことを意味します。IOW、ヒープ上にないもの。はい、オブジェクトの有効期間の問題も発生する可能性があります-シングルトンの破棄順序に関して-しかし、それらをヒープに貼り付けても、通常は何も解決しません。

于 2009-06-30T15:35:49.080 に答える
1

問題は、なぜすべてにポインタを使用するのかということです。スタックに割り当てられたオブジェクトは、作成がより安全で高速であるだけでなく、入力がさらに少なくなり、コードの見栄えが良くなります。

于 2009-06-30T17:20:32.917 に答える
1

質問を別の角度から見ると...

Foo *C++ では、ポインター ( ) と参照 ( )を使用してオブジェクトを参照できますFoo &。可能な限り、ポインターの代わりに参照を使用します。たとえば、関数/メソッドへの参照渡しの場合、参照を使用すると、コードで (うまくいけば) 次の仮定を行うことができます。

  • 参照されるオブジェクトは関数/メソッドによって所有されていないため、オブジェクトであってはなりませんdelete。「ほら、このデータ使って、使い終わったら返して」みたいな。
  • NULL ポインター参照の可能性は低くなります。NULL 参照が渡される可能性がありますが、少なくとも関数/メソッドのせいではありません。参照を新しいポインター アドレスに再割り当てすることはできないため、コードが誤って NULL またはその他の無効なポインター アドレスに再割り当てして、ページ フォールトが発生することはありません。
于 2009-06-30T16:02:40.103 に答える
0

スタック上に作成されたオブジェクトは、割り当てられたオブジェクトよりも速く作成されます。

なんで?

(デフォルトのメモリマネージャを使用した)メモリの割り当てには時間がかかるため(空のブロックを見つけたり、そのブロックを割り当てたりするため)。

また、スタックオブジェクトはスコープ外になると自動的に破棄されるため、メモリ管理の問題は発生しません。

ポインタを使用しない場合、コードは単純になります。デザインでスタックオブジェクトを使用できる場合は、それを使用することをお勧めします。

私自身は、スマートポインタを使用して問題を複雑にすることはありません。

OTOH私は埋め込みフィールドで少し作業しましたが、スタック上にオブジェクトを作成することはあまり賢くありません(各タスク/スレッドに割り当てられるスタックはそれほど大きくないため、注意する必要があります)。

したがって、それは選択と制限の問題であり、それらすべてに適合する応答はありません。

そして、いつものように、可能な限りシンプルに保つことを忘れないでください。

于 2009-06-30T20:46:03.810 に答える
0

私が新しい C++ プログラマーだった (そしてそれが私の最初の言語だった) とき、それは私を大いに混乱させました。一般的に2つのカテゴリのいずれかに分類されるように見える非常に悪いC ++チュートリアルがたくさんあります.「C / C ++」チュートリアルは、実際にはCチュートリアル(おそらくクラスを含む)であることを意味し、C ++は削除を伴うJavaであると考えるC ++チュートリアルです。 .

コードのどこかに「new」と入力するのに(少なくとも)約1〜1.5年かかったと思います。vector などの STL コンテナーを頻繁に使用していましたが、これは面倒でした。

多くの回答は、これを回避する方法を無視するか、直接言うことを避けているように見えると思います。通常、コンストラクタで new を使用して割り当て、デストラクタで delete を使用してクリーンアップする必要はありません。代わりに、オブジェクト自体を (オブジェクトへのポインターではなく) クラスに直接貼り付けて、コンストラクターでオブジェクト自体を初期化することができます。次に、ほとんどの場合、デフォルトのコンストラクターが必要なすべてを行います。

これが機能しないほとんどすべての状況 (たとえば、スタック領域が不足する可能性がある場合) では、おそらく標準コンテナーのいずれかを使用する必要があります: std::string、std::vector、および std:: map は私が最も頻繁に使用する 3 つですが、std::deque と std::list も非常に一般的です。他のもの ( std::set や非標準のlopeなど) はあまり使用されていませんが、同様に動作します。それらはすべてフリー ストアから割り当てられます (他の言語では「ヒープ」を表す C++ の用語)。次を参照してください: C++ STL の質問: アロケータ

于 2011-11-29T23:29:11.920 に答える
0

基本的に、生のポインターを使用する場合、RAII はありません。

于 2009-07-01T14:36:49.303 に答える
0

私が言及していないのは、メモリ使用量の増加です。4バイトの整数とポインタを想定

Pixel p;

8バイトを使用し、

Pixel* p = new Pixel();

12 バイトを使用し、50% 増加します。512x512 の画像に十分な量を割り当てるまで、それほど多くはないように思えます。次に、3MB ではなく 2MB を話しています。これは、これらすべてのオブジェクトでヒープを管理するオーバーヘッドを無視しています。

于 2009-06-30T17:27:48.960 に答える
-2

Pixelクラスにメンバーが追加されない限り、最初のケースが最適です。ますます多くのメンバーが追加されると、スタックオーバーフロー例外の可能性があります

于 2009-07-01T09:04:19.380 に答える