c++ 解放 なぜオブジェクト自体ではなくポインタを使うべきですか?




スマートポインタ デメリット (18)

ポインタは、オブジェクトのメモリ位置を直接参照します。Javaにはこれと似たものはありません。Javaには、オブジェクトの位置をハッシュテーブルで参照する参照があります。Javaでは、これらの参照を使用してポインタ演算などの操作を行うことはできません。

あなたの質問に答えるために、あなたの好みです。私はJavaのような構文を使うのが好きです。

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

Object *myObject = new Object;

のではなく:

Object myObject;

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

myObject.testFunc();

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

myObject->testFunc();

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


この質問には、前方宣言、多型などの重要な使用例を含む多くの優れた答えがありますが、あなたの質問の「魂」の一部が答えられないと感じます。つまりJavaとC ++で異なる構文が意味するものです。

2つの言語を比較してみましょう:

Java:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

これに最も近いのは以下の通りです:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

代わりのC ++の方法を見てみましょう:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

C ++はオブジェクトへのポインタやオブジェクト自体を扱うことができるのに対し、多かれ少なかれJava(暗黙的に)オブジェクトへのポインタを処理することが最も良い方法です。 これには例外があります。たとえば、Javaの「プリミティブ」型を宣言すると、実際の値はポインタではなくコピーされます。 そう、

Java:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

つまり、ポインターを使うことは、物事を扱う正しい方法か間違った方法のどちらかではありません。 しかし、他の答えは満足にカバーしています。 しかし、一般的な考え方では、C ++では、オブジェクトの存続期間とどこで生きるかについて、より多くの制御ができます。

Object * object = new Object()構文は、実際には典型的なJava(またはその意味ではC#)セマンティクスに最も近いものです。


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


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

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

ダイナミックアロケーションが頻繁に見られることは非常に残念です。 これは、悪いC ++プログラマがどれくらいいるかを示しています。

ある意味では、2つの質問が1つにまとめられています。 最初は動的割り当て( newを使用)を使用する必要がありますか? 2番目はポインタを使うべきときです。

重要なテイク・ホーム・メッセージは、ジョブに常に適切なツールを使用する必要があるということです。 ほぼすべての状況で、手動の動的割り当てや生ポインタを使用するよりも、より適切で安全な方法があります。

動的割り当て

あなたの質問では、オブジェクトを作成する2つの方法を実証しました。 主な違いは、オブジェクトの保存期間です。 Object myObject;実行しているときObject myObject; ブロック内では、オブジェクトは自動保存期間で作成されます。つまり、スコープから外れると自動的に破棄されます。 new Object()と、オブジェクトに動的な保存期間が設定されます。これは、明示的にdeleteまで生き続けることを意味します。 必要なときにのみダイナミックストレージ期間を使用する必要があります。 つまり、 できる限り自動的に保存期間を持つオブジェクトを作成するほうがよいでしょう

動的割り当てが必要な主な2つの状況:

  1. 現在のスコープよりも長生きするオブジェクト、つまりその特定のメモリ位置にある特定のオブジェクトであり、そのコピーではありません。 オブジェクトをコピー/移動するのが大丈夫なら(ほとんどの場合、あなたがすべきである)、自動オブジェクトを好むべきです。
  2. たくさんのメモリを割り当てる必要があります。これは簡単にスタックを埋めるかもしれません。 実際にはC ++の範囲外であるため、これを気にする必要がなければ(基本的にはそうする必要はありませんが)大丈夫ですが、残念ながら私たちはシステムの現実に対処しなければなりません〜を開発しています。

動的割り当てが絶対に必要な場合は、スマートポインタまたはRAIIを実行する他のタイプ(標準コンテナと同様)にカプセル化する必要があります。 スマートポインタは、動的に割り当てられたオブジェクトの所有権セマンティクスを提供します。 たとえば、 std::unique_ptrstd::shared_ptr見てみましょう。 それらを適切に使用すると、独自のメモリ管理の実行をほぼ完全に避けることができます(「 ゼロルール」を参照)。

ポインタ

しかし、ダイナミックアロケーションを超えたローポインタの他のより一般的な用途がありますが、ほとんどの場合、あなたが好むべき選択肢があります。 以前と同じように、 ポインタが本当に必要な場合を除き常に選択肢を優先します

  1. 参照セマンティクスが必要です 。 場合によっては、ポインターを使用してオブジェクトを渡すこともできます(割り当てられているかどうかは関係ありません)。 しかし、ほとんどの状況では、ポインターへの参照型を優先するべきです。これは具体的に設計されているためです。 これは、上記の状況1のように、必ずしも現在のスコープを超えてオブジェクトの存続期間を延ばすことに関するものではないことに注意してください。 前と同じように、オブジェクトのコピーを渡しても問題がなければ、参照セマンティクスは必要ありません。

  2. 多型性が必要です。 オブジェクトへのポインタまたは参照を介して、関数を多態的に(つまり、オブジェクトの動的なタイプに従って)呼び出すことができます。 それが必要な振る舞いであれば、ポインタや参照を使う必要があります。 再度、参照が好ましいはずである。

  3. オブジェクトが省略されているときにnullptrが渡されるようにすることで、オブジェクトがオプションであることを表現したいとします。 引数の場合は、デフォルトの引数や関数のオーバーロードを使用することをお勧めします。 それ以外の場合は、 std::optional (C ++ 17で導入されました。以前のC ++標準ではboost::optional使用しboost::optional )など、この動作をカプセル化する型を使用することをお勧めします。

  4. コンパイル時間を短縮するためにコンパイル単位をデカップリングする必要があります。 ポインターの有用なプロパティは、ポインティング・タイプの前方宣言のみを必要とすることです(実際にオブジェクトを使用するには、定義が必要です)。 これにより、コンパイルプロセスの一部を切り離すことができ、コンパイル時間を大幅に短縮できます。 Pimplイディオムを参照してください。

  5. CライブラリまたはCスタイルのライブラリとのインタフェースが必要です。 この時点で、生のポインタを使用する必要があります。 あなたができる最善のことは、最後の可能な瞬間に生ポインタを緩くすることだけを確実にすることです。 例えば、 getメンバ関数を使って、スマートポインタから生ポインタを得ることができます。 ライブラリーがハンドルを介して割り振りを解除すると予想される割り振りを実行している場合、オブジェクトを適切に割り振り解除するカスタム・デリーターを持つスマート・ポインターでハンドルをラップすることができます。


Object *myObject = new Object;

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

Object myObject;

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


C ++では、スタックに割り当てられたObject object;Object object;を使用して、ブロック内のステートメント)は宣言されたスコープ内にのみ存在します。コードブロックの実行が終了すると、宣言されたオブジェクトは破棄されます。 一方、 Object* obj = new Object()を使用してヒープにメモリを割り当てると、 delete objを呼び出すまでメモリはヒープに存続します。

ヒープ上にオブジェクトを作成するのは、宣言された/割り当てられたコードのブロックだけでなく、オブジェクトを使用したいときです。


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

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

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


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

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

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作成ではなく、直接、またはリソースクラス内で作成されません)が破棄されます。


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

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

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

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のようにコンテナに格納されることが多く、オブジェクトの寿命を自動的に管理します。


主な質問は、なぜオブジェクト自体ではなくポインタを使うべきなのでしょうか?そして、私の答えは、C ++にはreferencesがあるため、ポインターと同じパフォーマンスを保証します。

あなたの質問で言及したもう一つのこと:

Object *myObject = new Object;

どのように機能するのですか?それはObjectタイプのポインタを作成し、1つのオブジェクトに合うようにメモリを割り当て、デフォルトのコンストラクタを呼び出します。しかし、実際にはあまりうまくいかないので、動的にメモリ(キーワードを使用new)を割り当てた場合、メモリを手動で解放する必要があります。つまり、コードでは次のようになります。

delete myObject;

これはデストラクタを呼び出して、メモリを解放し、簡単に見えます、しかし、大規模なプロジェクトでは難しいかもしれない一つのスレッドがメモリを解放かどうかを検出するが、その目的のためにあなたが試すことができます共有ポインタを、これらがわずかにパフォーマンスが低下しますが、で動作する方がはるかに簡単ですそれら。

そして今、いくつかの紹介は終わり、質問に戻る。

関数の間でデータを転送しながら、オブジェクトの代わりにポインタを使用してパフォーマンスを向上させることができます。

見て、あなたはstd::string(それはまたオブジェクトです)、それは本当に多くのデータ、例えばビッグXMLを含んでいます、今あなたはそれを解析する必要がありますが、それのためにあなたはvoid foo(...)さまざまな方法で宣言することができる関数を持っています:

  1. void foo(std::string xml); この場合は、変数から関数スタックにすべてのデータをコピーしますが、時間がかかるため、パフォーマンスが低下します。
  2. void foo(std::string* xml);この場合、size_t変数を渡すのと同じ速度でオブジェクトにポインタを渡しますが、NULLポインタや無効なポインタを渡すことができるため、この宣言はエラーが発生しやすくなります。C参照を持たないために通常使用されるポインタ。
  3. void foo(std::string& xml); ここでは参照を渡しますが、基本的にはポインタを渡すのと同じですが、コンパイラはいくつかのことを行い、無効な参照を渡すことはできません(実際は無効な参照で状況を作成することはできますが、
  4. void foo(const std::string* xml); ここでは秒と同じですが、ポインタの値は変更できません。
  5. void foo(const std::string& xml); ここでは3番目と同じですが、オブジェクト値を変更することはできません。

より多くの私が言及したいと思いますどのような、あなたは関係なく、あなたが(と選択している配分方法のデータを渡さないためにこれらの5つの方法を使用することができますnewまたは定期的)。

言い換えれば、通常の方法でオブジェクトを作成すると、メモリをスタックにnew割り当てることができますが、ヒープを割り当てることでメモリを作成することができます。スタックを割り当てる方がはるかに高速ですが、本当に大きなデータ配列の場合は少し小さいので、大きなオブジェクトが必要な場合はヒープを使用する必要があります。スタックオーバーフローが発生する可能性がありますが、通常この問題はSTLコンテナを使用して解決さstd::stringまたコンテナです、いくつかの人はそれを忘れて:)


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


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

私は単純な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倍の増加しか見せていませんが、これはまだ相当です。


序文

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する責任がある場合、エラーリークが発生する可能性があります。 ポインターがホットポテトのように回っていると、オブジェクトの所有権を追跡するのが難しい場合があります。 スタック変数を使うのは、それがより簡単で良いからです。


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


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

Class Base1 {
};

Class Derived1 : public Base1 {
};


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

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

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


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





c++11