c++ - 解放 - スマートポインタ デメリット




なぜオブジェクト自体ではなくポインタを使うべきですか? (15)

私はJavaの背景から来て、C ++でオブジェクトの作業を開始しました。 しかし、私に起こったことの1つは、オブジェクト宣言のように、オブジェクト自体ではなくオブジェクトへのポインタを使用することが多いということです。

Object *myObject = new Object;

のではなく:

Object myObject;

関数を使用する代わりに、次のようにtestFunc()としましょう:

myObject.testFunc();

私たちは次のように書く必要があります

myObject->testFunc();

しかし私はなぜこのようにしなければならないのか分かりません。 私はそれがメモリアドレスに直接アクセスできるので効率とスピードに関係していると思います。 私は正しい?


しかし、なぜ私たちはこれをこのように使うべきなのか理解できません。

あなたが使用している場合は、関数本体の中でどのように動作するかを比較します:

Object myObject;

関数の中で、この関数が返ったら、あなたのmyObjectは破壊されます。 これは、あなたの関数の外にオブジェクトを必要としない場合に便利です。 このオブジェクトは現在のスレッドスタックに置かれます。

関数本体の中に書く場合:

 Object *myObject = new Object;

myObjectが指すObjectクラスのインスタンスは、関数が終了すると破棄されず、割り当てはヒープ上にあります。

今あなたがJavaプログラマであれば、2番目の例はJavaでのオブジェクト割り当ての仕組みに近いです。 この行は次のとおりObject *myObject = new Object; java: Object myObject = new Object();と等価Object myObject = new Object(); 。 違いは、java myObjectの下ではガベージコレクトされ、C ++では解放されませんが、明示的に `delete myObject; それ以外の場合は、メモリリークが発生します。

c ++ 11以降、動的な割り当ての安全な方法を使用できます:shared_ptr / unique_ptrに値を格納することでnew Object

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

また、オブジェクトは、map-sやvector-sのようにコンテナに格納されることが多く、オブジェクトの寿命を自動的に管理します。


序文

Javaは誇大宣伝とは異なり、C ++のようなものではありません。 Javaのハイパーマシンは、JavaにはC ++のような構文があるので、言語は似ていると信じてほしいです。 真実からは何もできません。 この誤解は、JavaプログラマがC ++に行きコードの意味を理解せずにJavaのような構文を使用する理由の一部です。

私たちは行く

しかし私はなぜこのようにしなければならないのか分かりません。 私はそれがメモリアドレスに直接アクセスできるので効率とスピードに関係していると思います。 私は正しい?

それどころか、実際には。 スタックはヒープに比べて非常に単純なので、 ヒープはスタックよりはるかに遅いです。 自動ストレージ変数(スタック変数とも呼ばれる)は、スコープを外れたときに呼び出されるデストラクタを持っています。 例えば:

{
    std::string s;
}
// s is destroyed here

一方、動的に割り当てられたポインタを使用する場合は、そのデストラクタを手動で呼び出す必要があります。 deleteはこのデストラクタを呼び出します。

{
    std::string* s = new std::string;
}
delete s; // destructor called

これは、C#とJavaで広く使われているnew構文とは関係ありません。 それらは全く異なる目的のために使用されています。

動的割り当てのメリット

1.事前に配列のサイズを知る必要はありません

多くのC ++プログラマーが最初に遭遇する問題の1つは、ユーザーからの任意の入力を受け入れるときは、スタック変数に固定サイズしか割り当てることができないということです。 配列のサイズも変更できません。 例えば:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

もちろん、 std::string代わりに使用した場合、 std::string内部的にサイズ変更されるため、問題ではありません。 しかし、本質的にこの問題の解決策は動的割り当てです。 ユーザーの入力に基づいて動的メモリを割り当てることができます。たとえば、次のようになります。

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

注意 :初心者の多くが間違っているのは、可変長配列の使い方です。 これはGNUの拡張であり、GCCの拡張の多くを反映しているため、Clangの拡張でもあります。 したがって、以下のint arr[n]は信頼されるべきではありません。

ヒープはスタックよりもはるかに大きいので、スタックには制限がありますが、ヒープは必要なだけ多くのメモリを任意に割り当てる/再割り当てできます。

2.配列はポインタではありません

これはどのようにあなたが求めるメリットですか? 配列とポインタの背後にある混乱/誤解を理解すれば、その答えは明確になります。 一般的には同じものとみなされますが、そうではありません。 この誤解は、ポインタが配列のように添え字付けることができるという事実と、配列が関数宣言のトップレベルのポインタに崩壊するために起こります。 しかしながら、配列が一旦ポインタに崩壊すると、ポインタはそのsizeof情報を失う。 したがって、 sizeof(pointer)sizeof(pointer)のサイズをバイト単位で表します。通常、64ビットシステムでは8バイトです。

配列に代入することはできず、配列を初期化するだけです。 例えば:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

一方、ポインタで何でもできます。 残念ながら、ポインタと配列の区別はJavaとC#では手を振っているので、初心者はその違いを理解していません。

3.多型

JavaおよびC#には、オブジェクトを別のものとして扱うための機能があります。たとえば、 asキーワードを使用します。 だから、もし誰かがEntityオブジェクトをPlayerオブジェクトとして扱いたいのなら、 Player player = Entity as Player;行うことができますPlayer player = Entity as Player; これは、特定の型にのみ適用する必要がある同種のコンテナで関数を呼び出す場合に非常に便利です。 機能は、以下の同様の方法で達成することができます。

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

三角関数だけがRotate関数を持っていれば、クラスのすべてのオブジェクトに対して呼び出すとコンパイルエラーになります。 dynamic_castを使用すると、 asキーワードをシミュレートできます。 明確にするために、キャストに失敗すると、無効なポインタを返します。 So !testは、 testがNULLか無効なポインタかどうかを確認するための略語です。つまり、キャストが失敗したことを意味します。

自動変数の利点

ダイナミックアロケーションができるすばらしいことをすべて見た後、あなたはたぶんダイナミックアロケーションをいつも使っていないのでしょうか? 私はすでにあなたに一つの理由を言った、ヒープは遅いです。 あなたがその記憶をすべて必要としないなら、それを虐待すべきではありません。 だからここにいくつかの不利な点があります。

  • エラーが発生しやすいです。 手動でメモリを割り当てるのは危険で、漏れが発生する可能性があります。 あなたがデバッガまたはvalgrind (メモリリークツール)を使用することに堪能でない場合は、あなたの頭の中からあなたの髪を引き出すかもしれません。 幸いなことに、RAIIのイディオムやスマートポインタはこれを少し緩和しますが、3つのルールや5つのルールなどのプラクティスに精通している必要があります。 取り込むべき情報がたくさんあり、知らない人や気にしない人の初心者はこのトラップに入るでしょう。

  • それは必要ない。 JavaやC#とは異なり、どこでもnewキーワードを使用するのは慣れていますが、C ++では、必要な場合にのみ使用してください。 あなたがハンマーを持っていれば、すべてが爪のように見えます。 C + +で始まる初心者はポインタを怖がって、スタック変数を習慣で使うことを学ぶのに対し、JavaやC#プログラマはポインタを理解せずに使うことから始まります。 それは文字通り間違った足で踏み込んでいます。 文法は一つのことなので、知っているすべてのものを放棄しなければなりません。言語を学ぶことはもう一つです。

1.(N)RVO - Aka、(Named)戻り値の最適化

多くのコンパイラが行う最適化の1つに、 elision戻り値の最適化というものがあります 。 これらのことは、多くの要素を含むベクトルなど、非常に大きなオブジェクトに役立つ不要なコピーを取り除くことができます。 通常、大きなオブジェクトをコピーして移動させるのではなく、ポインタを使用して所有権転送するのが一般的です。 これは移動セマンティクススマートポインタの導入につながりました。

ポインタを使用している場合、(N)RVOは発生しません 。 最適化が心配されている場合は、ポインタを返すか渡すのではなく、(N)RVOを利用する方が、より有益でエラーを起こしにくくなります。 関数の呼び出し側が動的に割り当てられたオブジェクトなどをdeleteする責任がある場合、エラーリークが発生する可能性があります。 ポインターがホットポテトのように回っていると、オブジェクトの所有権を追跡するのが難しい場合があります。 スタック変数を使うのは、それがより簡単で良いからです。


C ++では、ポインタ、参照、および値の3つの方法でオブジェクトを渡すことができます。 Javaは後者のものを制限します(唯一の例外はint、booleanなどのプリミティブ型です)。 奇妙なおもちゃのようなものではなく、C ++を使いたい場合は、これら3つの方法の違いを知ることをお勧めします。

Javaは「誰といつこれを破壊すべきか?」というような問題はないと主張する。 答えは:ガベージコレクター、素晴らしいと驚くほど。 それにもかかわらず、メモリリークに対して100%の保護を提供することはできません(はい、 Java メモリリークする可能性があります )。 実際には、GCはあなたに誤った安全感を与えます。 あなたのSUVが大きければ大きいほど、避難者への道は長くなります。

C ++では、オブジェクトのライフサイクル管理に対峙しています。 まあ、それに対処する手段があります( スマートポインタファミリ、QtのQObjectなど)。しかし、そのようなものはどれもGCのように「火の玉」では使用できませ常にメモリ処理を念頭に置いてください。 オブジェクトを破壊することに気を配っているだけでなく、同じオブジェクトを複数回破壊することも避けなければなりません。

まだ怖がっていない? OK:循環参照 - 人間自身で処理します。 そして、覚えておいてください:各オブジェクトを正確に一度削除してください。C ++のランタイムは死体を混乱させ、死んだものだけを残している人を好きではありません。

だから、あなたの質問に戻る。

オブジェクトをポインタや参照ではなく値渡しするときは、オブジェクトをコピーします(バイト数が多いか、データベースのダンプが多いかどうかにかかわらず、オブジェクト全体をコピーします。後者を避けるために十分スマートです。あなたは?)を行うたびに。 オブジェクトのメンバーにアクセスするには、 '。'を使用します。 (ドット)。

オブジェクトをポインタで渡すと、数バイト(32ビットシステムでは4、64ビットシステムでは8)だけコピーされます。つまり、このオブジェクトのアドレスです。 そして、これを誰にでも見せるために、あなたはメンバーにアクセスするときにこの派手な ' - >'演算子を使用します。 または '*'と '。'の組み合わせを使用することもできます。

参照を使うと、値のふりをするポインタが得られます。 これはポインタですが、 '。'でメンバーにアクセスします。

もう一度あなたの心を吹き飛ばしてください:コンマで区切られた複数の変数を宣言すると、(手を見て):

  • みんなにタイプが与えられる
  • 値/ポインタ/参照修飾子は個別です

例:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

ポインタを使って

  • 直接メモリに話すことができます。

  • ポインタを操作してプログラムのメモリリークを防ぐことができます。


すでに多くの優れた回答がありますが、私にあなたの一例を挙げましょう:

私は単純なItemクラスを持っています:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

私はそれらの束を保持するベクトルを作成します。

std::vector<Item> inventory;

私は100万のItemオブジェクトを作成し、それらをベクトルに戻します。私は名前でベクトルをソートし、特定の項目名に対して単純な反復バイナリ検索を行います。私はプログラムをテストし、実行を完了するまでに8分以上かかります。次に、在庫ベクトルを次のように変更します。

std::vector<Item *> inventory;

新しいものを使って100万個のItemオブジェクトを作成します。私のコードに行う変更は、最後にメモリクリーンアップのために追加するループを除いて、Itemsへのポインタを使用することだけです。そのプログラムは40秒未満で実行されるか、10倍の速度向上よりも優れています。編集:コードはhttp://pastebin.com/DK24SPeWにあります。コンパイラの最適化では、テストしたばかりのマシンでわずか3.4倍の増加しか見せていませんが、これはまだ相当です。


オブジェクトへのポインタを使用することには多くの利点があります。

  1. 効率性(あなたがすでに指摘したように)。オブジェクトを関数に渡すと、オブジェクトの新しいコピーが作成されます。
  2. サードパーティライブラリからのオブジェクトの操作。あなたのオブジェクトが第三者のコードに属していて、作成者がポインタだけでオブジェクトの使用を意図している場合(コピーコンストラクタなどはありません)、このオブジェクトを渡す唯一の方法はポインタを使用することです。値を渡すと問題が発生する可能性があります。(ディープコピー/シャローコピーの問題)。
  3. オブジェクトがリソースを所有していて、所有権を他のオブジェクトと一緒にsahredしないようにしたい場合。

ポインタには多くのユースケースがあります。

多型的な振る舞い 。 多態型では、スライスを避けるためにポインタ(または参照)が使用されます。

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

参照セマンティクスとコピーの回避 。 非多型型の場合、ポインタ(または参照)は潜在的に高価なオブジェクトのコピーを避けます

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

C ++ 11には高価なオブジェクトのコピーを関数の引数に、戻り値として避けることができる移動セマンティクスがあることに注意してください。 しかし、ポインタを使用すると、それらを避けることができ、同じオブジェクトに対して複数のポインタを使用できます(オブジェクトは一度にしか移動できません)。

リソースの獲得new演算子を使用してリソースへのポインタを作成することは、現代のC ++の反パターンです。 特別なリソースクラス(Standardコンテナの1つ)またはスマートポインタstd::unique_ptr<>またはstd::shared_ptr<> )を使用します。 検討してください:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

未処理のポインタは、直接の作成や暗黙的な戻り値による所有ではなく、「ビュー」としてのみ使用する必要があります。 C ++ FAQのこのQ&Aも参照してください

より詳細なライフタイム制御共有ポインタが(関数引数として)コピーされるたびに、そのポインタが指しているリソースは生きています。 スコープ外に出ると、通常のオブジェクト( new作成ではなく、直接、またはリソースクラス内で作成されません)が破棄されます。


ポインタを使うもう一つの理由は、 前方宣言のためです 。 十分な大きさのプロジェクトでは、実際にコンパイル時間を短縮できます。


"必要は発明の母。" 私が指摘したいと思う重要な違いの大部分は、私自身のコーディング経験です。場合によっては、オブジェクトを関数に渡す必要があります。その場合、あなたのオブジェクトが非常に大きなクラスの場合は、オブジェクトとして渡すとオブジェクトのコピーにオーバーヘッドが発生しますので、その状態をコピーします(あなたは望みません.AND BIG OVERHEAD)。 4バイトサイズ(32ビットと仮定)。他の理由はすでに上記のとおりです...


あなたはしないでください。人々(多くの人々、悲しいことに)は無知からそれを書きます。

時にはダイナミックな割り当てがありますが、あなたが与える例では間違っています。

効率について考える場合は、正当な理由がないため間接参照が導入されるため、これは悪化します。このようなプログラミングは、遅くなり、エラーを起こしやすくなります。


これは長らく議論されていますが、Javaではすべてがポインタです。スタックとヒープの割り当ては区別されません(すべてのオブジェクトがヒープ上に割り当てられます)ので、ポインタを使用していることはわかりません。C ++では、メモリ要件に応じて2つを混在させることができます。パフォーマンスとメモリ使用量は、C ++(duh)の方がより決定的です。


ポインタの重要な使用例を1つ含める。いくつかのオブジェクトを基底クラスに格納しているときには、それはポリモフィックである可能性があります。

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

この場合、bObjを直接オブジェクトとして宣言することはできません。ポインタを持つ必要があります。


ポインタを使用する1つの理由は、C関数とのインタフェースです。別の理由は、メモリを節約することです。たとえば、多くのデータを含むオブジェクトを渡すのではなく、関数にプロセッサ集約型コピーコンストラクタを持たせるのではなく、オブジェクトへのポインタを渡すだけで、特にループ中にメモリと速度を節約できます。 Cスタイルの配列を使用している場合を除き、参照はその場合に適しています。


メモリ使用率が高い地域では、ポインタが便利です。たとえば、数千のノードが再帰ルーチンを使用して生成され、後でそれらを使用してゲームにおける次の最良の移動を評価するミニマックスアルゴリズムを考えてください。スマートポインタのように割り当てを解除またはリセットする能力がメモリ消費を大幅に削減します。非ポインタ型変数は、再帰呼び出しが値を返すまで、領域を占有し続けます。


Object *myObject = new Object;

これにより、メモリリークを避けるために明示的に削除する必要がある(ヒープ上の)オブジェクトへの参照が作成されます。

Object myObject;

これを実行すると、オブジェクト(myObject)が有効範囲外になったときに自動的に削除される(スタック上の)自動タイプのオブジェクト(myObject)が作成されます。





c++11