web-dev-qa-db-ja.com

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

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

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つはヒープ上にあるため、後で削除する必要があります。 2つの間に基本的な違いはありますか?なぜ私は他のものよりも好むべきですか?

74
Eric

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

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

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

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クラスは内部的にヒープメモリを割り当てますが、そのデストラクタがクリーンアップを処理するため、クラスをusingする場合、それについて心配してください。 (一般的な原則を示すために、ここでの最後の例は大幅に簡略化されていることをおそらく言及する必要があります。このクラスを実際に使用すると、いくつかの考えられるエラーも含まれます。yの割り当てが失敗した場合、xは解放されません、そしてPixelがコピーされると、両方のインスタンスが同じデータを削除しようとします。そのため、最後の例を細かく見ていきます。実際のコードは少し複雑ですが、一般的な考え方を示しています)

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

186
jalf

削除を追加するまで、それらは同じではありません。
あなたの例はささいなことですが、デストラクタには実際に実際の作業を行うコードが含まれている場合があります。これは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の主な違いは次のとおりです:

ポインターの所有者の概念。オブジェクトを適切なタイミングで削除するのは所有者の責任です。 rawポインターに関連付けられている所有権情報がないため、実際のプログラムではrawポインターがほとんど表示されないのはこのためです。代わりに、ポインターは通常、スマートポインターでラップされます。スマートポインターは、誰がメモリを所有するか、したがって誰がメモリをクリーンアップする責任があるかを意味します。

次に例を示します。

 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.

他にもあります。

30
Martin York

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

Java=バックグラウンドから来ている場合、割り当てられたものとそれを解放する責任があるかどうかを追跡することを中心にC++がどれだけ関与するかについて完全に準備できていない可能性があります。

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

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

25
Clyde

私は機会があればいつでも最初の方法を使うことを好む:

  • 速いです
  • メモリの割り当て解除について心配する必要はありません
  • pは、現在のスコープ全体に対して有効なオブジェクトになります
24
rpg

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

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

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

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

14
Tim

コード:

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

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

Newを使用するには、そのすべてのメモリ管理オーバーヘッドが必要です。

質問は次のようになります-データにスタック領域またはヒープ領域を使用しますか? 'p'のようなスタック(またはローカル)変数は逆参照を必要としませんが、newを使用すると間接層が追加されます。

11
Skizz

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

11
Steve

はい、最初は理にかなっています。JavaまたはC#のバックグラウンドから来ています。割り当てたメモリを解放することを覚えておかなければならないのは大したことではないようです。しかし、最初のメモリリークでは、すべてを解放したので頭を掻きます。2回目は発生し、3回目はさらにイライラします。最後に、メモリの問題による6か月の頭痛の後に、それに飽き始めたら、そのスタックに割り当てられたメモリはますます魅力的に見え始めます。どれほど素晴らしく、きれいなものか-スタックに置いてそれを忘れるだけです。かなりすぐに、いつでもスタックを使用するようになりますそれで逃れることができます。

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

10
eeeeaaii

すべてを新しいものにしない最大の理由は、スタック上に物事があるときに非常に確定的なクリーンアップを行えることです。 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として知られています-リソース割り当ては初期化であり、リソースの取得と破棄に対処する能力を大幅に向上させることができます。

6
Doug T.

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

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よりも遅くなる可能性があります。

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

ただし、ポインターを使用しない限り、どのような多態性も使用できません。オブジェクトには完全に静的な型があり、これはコンパイル時に決定されます。

6
BlackAura

問題はポインタではありませんそれ自体NULLポインタの導入を除いて)、手動でのメモリ管理です。

もちろんおもしろいのは、私が見たすべてのJavaチュートリアルで、ガベージコレクターがdeleteを呼び出すことを覚えておく必要がないため、とてもクールなホットだと説明したことです。 、実際にはC++はdeleteを呼び出すときにnewのみを必要とします(そしてdelete[]を呼び出すとnew[])。

4
Max Lybbert

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

「このパラメーターがnilの場合はどうなりますか?」これは参照を使用することを意味します。

ただし、本当にnilを渡せるようにしたい場合、参照は問題外です:)ポインターを使用すると、柔軟性が高まり、遅延を増やすことができます。これは本当に良いことです。割り当てる必要があることがわかるまで割り当てないでください!

4
ralphtheninja

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

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

4
Matt Brunell

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

彼らは遅いです。

コンパイラーの最適化はポインターアクセスシマンティックスほど効果的ではありません。多くのWebサイトでコンパイラーの最適化について読むことができますが、これはIntelのまともな pdfです

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

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

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

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

...このテーマのさまざまなバリエーション...

複雑なメモリ参照。または、言い換えると、複雑なポインター計算などの参照を分析すると、コンパイラーが効率的なコードを生成する能力に負担がかかります。コンパイラーまたはハードウェアがデータがどこにあるかを判別するために複雑な計算を実行しているコード内の場所に注目する必要があります。ポインターのエイリアシングとコードの簡略化は、コンパイラーがメモリーアクセスパターンを認識できるようにし、コンパイラーがメモリーアクセスとデータ操作をオーバーラップできるようにします。不要なメモリ参照を減らすと、ソフトウェアをパイプライン化する機能がコンパイラに公開されます。メモリ参照の計算を単純にしておくと、エイリアスや整列など、他の多くのデータ位置プロパティを簡単に認識できます。コンパイラーを支援するためには、メモリ参照を簡略化するための強度削減または帰納法の使用が重要です。

2

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

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

明確にするために、この文脈での「静的」とは、動的に割り当てられないことを意味します。 IOW、ヒープにないもの。はい、それらにはオブジェクトの存続期間の問題(シングルトン破壊順序の観点から)も含まれる可能性がありますが、それらをヒープに固定しても通常は何も解決されません。

2
Roddy

別の角度から質問を見て...

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

  • 参照されるオブジェクトは関数/メソッドによって所有されていないため、オブジェクトをdeleteにしないでください。 「ここでは、このデータを使用しますが、完了したら返してください」と言っているようなものです。
  • NULLポインターの参照は可能性が低くなります。 NULL参照を渡すことは可能ですが、少なくとも関数/メソッドのせいではありません。参照を新しいポインターアドレスに再割り当てできないため、コードが誤って参照をNULLまたはその他の無効なポインターアドレスに再割り当てして、ページフォールトを引き起こす可能性はありません。
1
spoulson

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

1

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

コードのどこかに「new」と入力するには、少なくとも1〜1.5年はかかったと思います。私はvectorのようなSTLコンテナーを頻繁に使用しました。

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

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

0
David Stone

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

0
anon

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

どうして?

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

また、スタックオブジェクトはスコープ外にあると自動的に自分自身を破壊するため、メモリ管理の問題はありません。

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

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

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

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

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

0
INS

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

Pixel p;

8バイトを使用し、

Pixel* p = new Pixel();

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

0
KeithB