web-dev-qa-db-ja.com

なぜC++プログラマは 'new'の使用を最小限にすべきでしょうか。

私はスタックオーバーフローの質問 std :: list <std :: string>を使用したときにstd :: stringでメモリリークが発生しています に出会い、 コメントの1つ はこう言っています。

そんなにnewを使わないでください。私があなたがしたどこでもあなたが新しいものを使った理由を見ることができません。あなたはC++で値によってオブジェクトを作成することができ、それは言語を使用することの大きな利点の一つです。あなたはすべてをヒープ上に割り当てる必要はありません。 Javaプログラマーのように考えるのをやめなさい。

それが彼が何を意味するのか、私にはよくわかりません。なぜオブジェクトは可能な限り頻繁にC++で値によって作成されるべきですか、そしてそれは内部的にどのような違いがありますか?答えを誤解しましたか?

802
bitgarden

2つの広く使用されているメモリ割り当て技術があります:自動割り当てと動的割り当て。一般に、それぞれに対応するメモリ領域があります。スタックとヒープです。

スタック

スタックは常にメモリを順番に割り当てます。逆の順序でメモリを解放する必要があるため、そうすることができます(先入れ先出し、先入れ先出し:FILO)。これは、多くのプログラミング言語におけるローカル変数のメモリ割り当て手法です。最低限の簿記を必要とし、割り当てる次のアドレスが暗黙のうちにあるのでそれは非常に、非常に速いです。

C++では、これは自動記憶域と呼ばれます。記憶域はスコープの最後で自動的に要求されるためです。現在のコードブロック({}で区切られている)の実行が完了するとすぐに、そのブロック内のすべての変数のメモリが自動的に収集されます。これは、リソースをクリーンアップするためにdestructors)が呼び出される瞬間でもあります。

ヒープ

ヒープにより、より柔軟なメモリ割り当てモードが可能になります。簿記はより複雑で、割り当ては遅くなります。暗黙の解放ポイントがないため、deleteまたはdelete[]Cのfree)を使用して手動でメモリを解放する必要があります。ただし、暗黙的なリリースポイントが存在しないことが、ヒープの柔軟性への鍵です。

動的割り当てを使用する理由

ヒープの使用が遅くなり、メモリリークやメモリの断片化を招く可能性がある場合でも、動的割り当ての使用方法はそれほど制限されていないため、完全に優れた使用例があります。

動的割り当てを使用する2つの主な理由:

  • コンパイル時に必要なメモリ量がわかりません。たとえば、テキストファイルを文字列に読み込むとき、通常ファイルのサイズがわからないため、プログラムを実行するまで割り当てるメモリの量を決定できません。

  • 現在のブロックを離れた後も持続するメモリを割り当てます。例えば、ファイルの内容を返すstring readfile(string path)という関数を書きたいと思うかもしれません。この場合、スタックがファイルの内容全体を保持できたとしても、関数から戻って割り当てられたメモリブロックを保持することはできません。

動的割り当てが不要な理由

C++では、_(destructorと呼ばれるきれいな構造体があります。このメカニズムにより、リソースの有効期間を変数の有効期間に合わせることでリソースを管理できます。この手法は _ raii _ と呼ばれます。 C++のポイントリソースをオブジェクトに「ラップ」するstd::stringはその好例です。

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

実際には可変量のメモリを割り当てます。 std::stringオブジェクトはヒープを使ってメモリを割り当て、デストラクタで解放します。この場合、notを手動で管理する必要はありませんでしたが、それでも動的メモリ割り当ての利点が得られました。

特に、このスニペットでは、

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

不必要な動的メモリ割り当てがあります。このプログラムでは、もっとタイピング(!)が必要で、メモリの割り当てを解除するのを忘れる危険性があります。それは明らかな利益なしにこれを行います。

自動ストレージをできるだけ頻繁に使用する理由

基本的に、最後の段落はそれをまとめたものです。できるだけ自動記憶域を使用すると、プログラムは次のようになります。

  • 入力が速いです。
  • 実行すると速くなります。
  • メモリ/リソースリークが発生しにくくなります。

ボーナスポイント

参照した質問では、さらに懸念があります。特に、次のクラス

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

実際には次のものよりもはるかに危険です。

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

その理由は、std::stringがコピーコンストラクタを正しく定義しているからです。次のプログラムを考えてください。

int main ()
{
    Line l1;
    Line l2 = l1;
}

元のバージョンを使用すると、このプログラムは同じ文字列に対してdeleteを2回使用するため、クラッシュする可能性があります。修正版を使用すると、各Lineインスタンスは独自の文字列instance、それぞれ独自のメモリを持ち、両方ともプログラムの終わりに解放されます)。

その他の注意

_ raii _ の広範な使用は、上記のすべての理由からC++でのベストプラクティスと考えられています。しかし、すぐには明らかではない追加の利点があります。基本的に、それはその部分の合計よりも優れています。全体のメカニズムcomposes。スケールします。

ビルディングブロックとしてLineクラスを使用する場合:

 class Table
 {
      Line borders[4];
 };

それから

 int main ()
 {
     Table table;
 }

4つのstd::stringインスタンス、4つのLineインスタンス、1つのTableインスタンス、およびすべての文字列の内容とすべては自動的に解放されます)を割り当てます。

960
André Caron

スタックは速くて確実なので

C++では、特定の関数内のすべてのローカルスコープオブジェクトに対して、スタック上で領域を割り当てるのに1つの命令しか必要とせず、そのメモリをリークすることは不可能です。そのコメントは、 「ヒープではなくスタックを使用する」のようなものを言うことを意図していました(または意図しているべきでした)。

162
DigitalRoss

それは複雑です。

まず、C++はガベージコレクションされていません。したがって、すべての新規ユーザーに対して、対応する削除が必要です。この削除を行わないと、メモリリークが発生します。さて、このような単純なケースでは:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

これは簡単です。しかし、「Doもの」が例外をスローした場合はどうなりますか?おっと:メモリリーク。 "Do stuff"がreturnを早く発行するとどうなりますか?おっと:メモリリーク。

そしてこれは 最も単純な場合 のためです。もしあなたがたまたまその文字列を誰かに返したら、今や彼らはそれを削除しなければなりません。そして彼らがそれを議論として渡すならば、それを受け取る人はそれを削除する必要がありますか?いつ削除するのですか。

または、これを実行するだけです。

std::string someString(...);
//Do stuff

いいえdelete。オブジェクトは「スタック」上に作成され、範囲外になると破棄されます。オブジェクトを返すこともできます。したがって、その内容を呼び出し元の関数に転送できます。オブジェクトを関数に渡すことができます(通常は参照またはconst-reference:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)など)。

すべてnewdeleteがありません。誰がメモリを所有しているのか、誰がメモリの削除を担当しているのかという問題はありません。もしあなたがそうするなら:

std::string someString(...);
std::string otherString;
otherString = someString;

otherStringsomeString data のコピーを持っていると理解されています。それはポインタではありません。それは別のオブジェクトです。それらは同じ内容を持っているかもしれませんが、他に影響を与えることなく一方を変更することができます。

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

アイデアを見ますか?

100
Nicol Bolas

newによって作成されたオブジェクトは、リークしないように、最終的にdeletedにする必要があります。デストラクタは呼び出されず、メモリも解放されません。 C++にはガベージコレクションがないので、それは問題です。

値によって作成されたオブジェクト(つまりスタック上のオブジェクト)は、範囲外になると自動的に終了します。デストラクタ呼び出しはコンパイラによって挿入され、メモリは関数が戻ると自動的に解放されます。

auto_ptrshared_ptrのようなスマートポインタは、ぶら下がっている参照問題を解決しますが、コーディング規則を必要とし、他の問題(コピー可能性、参照ループなど)を持ちます。

また、非常にマルチスレッドのシナリオでは、newがスレッド間の競合のポイントです。 newを使い過ぎるとパフォーマンスに影響が出る可能性があります。各スレッドがそれ自身のスタックを持っているので、スタックオブジェクトの作成は定義上スレッドローカルです。

値オブジェクトのマイナス面は、Host関数が戻ると死ぬことです - 値をコピーするか値で返すだけでは、呼び出し元にそれらへの参照を渡すことはできません。

71
Seva Alekseyev
  • C++は独自にメモリマネージャを採用していません。 C#のような他の言語は、Javaがメモリを処理するためのガベージコレクタを持っています
  • オペレーティングシステムルーチンを使用してメモリを割り当て、new/deleteが多すぎると、使用可能なメモリが断片化する可能性があります。
  • どのアプリケーションでも、メモリが頻繁に使用されている場合は、事前に割り当てて不要なときに解放することをお勧めします。
  • 不適切なメモリ管理はメモリリークを引き起こす可能性があり、追跡するのは本当に困難です。そのため、機能の範囲内でスタックオブジェクトを使用することは、実績のある手法です。
  • スタックオブジェクトを使用することの欠点は、戻り時、関数への受け渡し時などに、オブジェクトの複数のコピーが作成されることです。
  • C++では、メモリの割り当てと解放が2つの異なる場所で行われると、本当に面倒です。リリースの責任は常に問題であり、ほとんどの場合、一般的にアクセス可能なポインタ、スタックオブジェクト(最大可能)、およびauto_ptr(RAIIオブジェクト)などの技法に依存しています。
  • 最善のことは、あなたがメモリを管理しているということです、そして最悪のことは、アプリケーションに不適切なメモリ管理を採用している場合は、メモリを管理できないということです。メモリの破損によるクラッシュは最も厄介で追跡が困難です。
28
sarat

私は、できる限り少なく新しいことをするいくつかの重要な理由が見逃されているのを見ます:

演算子newの実行時間は確定的ではありません

newを呼び出すと、OSが新しい物理ページをプロセスに割り当てるようになる場合とそうでない場合があります。頻繁に行うと、非常に遅くなる可能性があります。あるいは、すでに適切なメモリの場所が用意されている場合もありますが、わかりません。プログラムに一貫した予測可能な実行時間(リアルタイムシステムやゲーム/物理シミュレーションなど)を持たせる必要がある場合は、タイムクリティカルなループでnewを回避する必要があります。

演算子newは暗黙のスレッド同期です

はい、あなたは私の言うことを聞きました、あなたのOSはあなたのページテーブルが矛盾していないことを確かめる必要があります、そのようにnewを呼び出すことはあなたのスレッドに暗黙のミューテックスロックを取得させます。多くのスレッドから一貫してnewを呼び出しているのであれば、実際にはスレッドをシリアル化しています(私はこれを32個のCPUで行い、それぞれnewをヒットして数百バイトを取得します)

遅い、断片化、エラーが発生しやすいなどの残りの部分は、他の回答で既に言及されています。

20
Emily L.

Pre-C++ 17:

結果をスマートポインターでラップしても、微妙なリークが発生しやすいため

オブジェクトをスマートポインターでラップすることを覚えている「注意深い」ユーザーを考えてみましょう。

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

このコードは、無保証であり、shared_ptrが構築されるbeforeT1またはT2。したがって、new T1()またはnew T2()のいずれかが他の成功後に失敗した場合、shared_ptrが破棄および割り当て解除するために存在しないため、最初のオブジェクトがリークされます。

解決策:make_sharedを使用します。

C++ 17以降:

これはもはや問題ではありません。C++ 17はこれらの操作の順序に制約を課します。この場合、new()への各呼び出しの直後に、他のスマートポインターの構築が続く必要があります。間の操作。これは、2番目のnew()が呼び出されるまでに、最初のオブジェクトが既にスマートポインターでラップされていることが保証されているため、例外がスローされた場合のリークを防ぎます。

C++ 17で導入された新しい評価順序のより詳細な説明は、Barry 別の回答 によって提供されました。

@ Remy Lebea に感謝します。これはC++ 17でのstillの問題であることを指摘しています(それほどではありませんが):shared_ptrコンストラクターは制御ブロックの割り当てとスローに失敗した場合、渡されたポインターは削除されません。

解決策:make_sharedを使用します。

18
Mehrdad

かなりの程度まで、それは誰かが彼ら自身の弱点を一般的な規則に引き上げることです。 それ自体 new演算子を使用してオブジェクトを作成することに問題はありません。いくつかの議論があるのは、あなたがいくつかの分野でそうしなければならないということです:あなたがオブジェクトを作成するならば、あなたはそれが破壊されることになるのを確実にする必要があります。

これを行う最も簡単な方法は、自動ストレージにオブジェクトを作成することです。そのため、C++では、範囲外になったときにそのオブジェクトを破棄することを認識しています。

 {
    File foo = File("foo.dat");

    // do things

 }

さて、あなたがエンドブレースの後にそのブロックから落ちるとき、fooは範囲外であることに注目してください。 C++はそのdtorを自動的に呼び出します。 Javaとは異なり、GCがそれを見つけるのを待つ必要はありません。

あなたが書いていた

 {
     File * foo = new File("foo.dat");

あなたはそれを明示的に一致させたいと思うでしょう

     delete foo;
  }

あるいはさらに良いことには、あなたのFile *を "スマートポインタ"として割り当ててください。気を付けていないと、水漏れの原因になります。

答え自体は、あなたがnewを使わないのならヒープに割り当てないという誤った仮定をしています。実際、C++ではそれを知りません。せいぜい、ごくわずかな量のメモリ、たとえば1ポインタがスタックに割り当てられていることをご存知でしょう。ただし、Fileの実装が次のようになっているかどうかを検討してください。

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

それからFileImpl まだ スタックに割り当てられます。

そしてはい、あなたは持っていることを確認した方がいいです

     ~File(){ delete fd ; }

クラスでも。それがなければ、 どうやら ヒープにまったく割り当てていなくても、ヒープからメモリをリークすることになります。

17
Charlie Martin

new() little としてできるだけ使用すべきではありません。それは 慎重に として可能な限り使用されるべきです。そしてそれは実用主義によって要求されるのと同じくらい必要なだけ使われるべきです。

暗黙的な破棄に依存する、スタック上のオブジェクトの割り当ては単純なモデルです。オブジェクトの必要なスコープがそのモデルに適合する場合は、関連するnew()とNULLポインタのチェックと共に、delete()を使用する必要はありません。スタック上に短期間のオブジェクト割り当てが多い場合は、ヒープの断片化の問題を軽減する必要があります。

しかし、オブジェクトの寿命を現在の範囲を超えて延長する必要がある場合は、new()が正しい答えです。削除されたオブジェクトやポインタの使用に伴う他のすべての問題点を使用して、いつ、どのようにdelete()を呼び出すか、およびNULLポインタの可能性に注意を払うようにしてください。

15

Newを使用すると、オブジェクトはヒープに割り当てられます。あなたが拡張を予想するとき、それは一般的に使われます。次のようなオブジェクトを宣言すると

Class var;

それはスタックに置かれます。

あなたはいつもnewを使ってヒープに置いたオブジェクトに対してdestroyを呼び出す必要があります。これはメモリリークの可能性を開きます。スタックに置かれたオブジェクトはメモリリークを起こしがちです。

13
Tim

ヒープの過剰使用を避けるための1つの注目すべき理由は、パフォーマンスのためです。特に、C++で使用されるデフォルトのメモリ管理メカニズムのパフォーマンスが関係しています。些細なケースでは割り当ては非常に高速ですが、厳密な順序なしでサイズが不均一なオブジェクトに対して多くのnewおよびdeleteを実行すると、メモリの断片化だけでなく、割り当てアルゴリズムも複雑になります。特定の場合にパフォーマンスを完全に破壊する可能性があります。

それは メモリプール という問題であり、従来のヒープ実装の固有の欠点を軽減しながら、必要に応じてヒープを使用できるようにするために作成されました。

ただし、この問題を完全に回避するには、なお良いでしょう。スタックに配置できる場合は、そうします。

11
tylerl

私は新しい「多すぎる」を使うという考えに反対する傾向があります。元のポスターがシステムクラスでnewを使用するのは少しばかげていますが。 (int *i; i = new int[9999];?本当に?int i[9999];の方がはるかに明確です。)thatがコメンターのヤギを獲得したと思います。

システムオブジェクトを使用している場合、まったく同じオブジェクトへの複数の参照が必要になることはveryまれです。値が同じである限り、それだけが重要です。また、システムオブジェクトは通常、メモリの多くの領域を占有しません。 (文字列内の文字ごとに1バイト)。そして、もしそうなら、ライブラリはそのメモリ管理を考慮に入れて設計する必要があります(うまく書かれている場合)。これらの場合(彼のコードのニュースの1つまたは2つを除くすべて)、newは実質的に無意味であり、混乱とバグの可能性を導入するのに役立ちます。

ただし、独自のクラス/オブジェクト(たとえば、元のポスターのLineクラス)を使用している場合は、メモリフットプリント、データの永続性などの問題について自分で考え始める必要があります。この時点で、同じ値への複数の参照を許可することは非常に貴重です-複数の変数が同じ値を持つだけでなく、正確に同じを参照する必要があるリンクリスト、辞書、グラフなどの構造を可能にしますオブジェクトメモリ内。ただし、Lineクラスにはこれらの要件はありません。したがって、元のポスターのコードには、実際にはnewはまったく必要ありません。

10
Chris Hayes

私はポスターがheapではなくYou do not have to allocate everything on thestackを言うことを意図していたと思います。

基本的に、アロケータによるかなりの作業を伴うヒープベースの割り当てではなく、スタック割り当てのコストが安いため、オブジェクトはスタックに割り当てられます(もちろんオブジェクトサイズが許す場合)。ヒープに割り当てられたデータを管理します。

10
Khaled Nassar

2つの理由

  1. この場合は不要です。あなたはあなたのコードを必要以上に複雑にしています。
  2. それはヒープ上のスペースを割り当てます、そしてそれは後でそれをdeleteすることを覚えておかなければならないことを意味します、さもなければそれはメモリリークを引き起こすでしょう。
3
Dan

newは新しいgotoです。

gotoがそれほど悪用されている理由を思い出してください。フロー制御のための強力で低レベルのツールである一方で、人々はそれを不必要に複雑な方法で使用し、コードを追跡するのを難しくしました。さらに、最も有用で読みやすいパターンは、構造化プログラミングステートメント(たとえば、forまたはwhile)にエンコードされました。究極の効果は、gotoが適切な方法であるコードがかなりまれであるということです。gotoを書きたがっている場合、あなたはおそらく物事をうまくやっていないでしょう(あなたが本当に何をしているのかを知ってください。

newも同様です。不必要に複雑で読みにくくするためによく使用されます。また、エンコードできる最も有用な使用パターンは、さまざまなクラスにエンコードされています。さらに、まだ標準クラスがない新しい使用パターンを使用する必要がある場合は、それらをエンコードする独自のクラスを作成できます!

newgotoステートメントをペアにする必要があるため、newdeleteよりもworseであるとさえ主張します。

gotoと同様、newを使用する必要があると思われる場合、おそらく悪いことをしていることになります。特に、必要な動的割り当てをカプセル化することを目的とするクラスの実装の外で行う場合。

2
Hurkyl

主な理由は、ヒープ上のオブジェクトは単純な値よりも使用および管理が常に困難だからです。読みやすく保守しやすいコードを書くことは、本格的なプログラマーにとって常に最優先事項です。

もう1つのシナリオは、私たちが使用しているライブラリが値の意味を提供し、動的割り当てを不要にすることです。 Std::stringは良い例です。

ただし、オブジェクト指向コードでは、ポインタを使用すること(事前に作成するためにnewを使用することを意味します)は必須です。リソース管理の複雑さを単純化するために、スマートポインタなど、できるだけ単純にするためのツールが多数あります。オブジェクトベースのパラダイムまたは一般的なパラダイムは、値のセマンティクスを前提としており、他のところで述べられているように、_ new _をほとんどまたはまったく必要としません。

伝統的なデザインパターン、特に GoF bookで言及されているものは典型的なOOコードなので、newを多用しています。

1
bingfeng zhao

上記のすべての正しい答えをもう1つ指摘すると、それはあなたがどんな種類のプログラミングをしているかによって異なります。たとえば、Windowsでのカーネル開発 - >スタックは非常に限られており、ユーザーモードのようにページフォルトを起こすことができないかもしれません。

そのような環境では、新しいまたはCのようなAPI呼び出しが推奨され、さらには必要とされます。

もちろん、これは単に規則の例外です。

1