私はC++を理解しようとしているCプログラマです。多くのチュートリアルでは、次のようなスニペットを使用したオブジェクトのインスタンス化を示します。
Dog* sparky = new Dog();
これは、後で行うことを意味します。
delete sparky;
理にかなっています。さて、動的なメモリ割り当てが不要な場合、上記の代わりに使用する理由はありますか
Dog sparky;
そして、sparkyがスコープから外れたらデストラクタを呼び出せますか?
ありがとう!
それどころか、経験則として、ユーザーコードに新規/削除を含めない範囲で、常にスタックの割り当てを優先する必要があります。
あなたが言うように、スタックで変数が宣言されると、そのデストラクタはスコープ外になると自動的に呼び出されます。これは、リソースの有効期間を追跡し、リークを回避するためのメインツールです。
そのため、一般的に、メモリ(newの呼び出しによる)、ファイルハンドル、ソケットなどに関係なく、リソースを割り当てる必要があるたびに、コンストラクタがリソースを取得し、デストラクタがそれを解放するクラスにラップします。その後、そのタイプのオブジェクトをスタック上に作成できます。これにより、リソースがスコープ外に出たときにリソースが解放されることが保証されます。そうすれば、メモリリークを回避するために、どこでも新しい/削除ペアを追跡する必要がなくなります。
このイディオムの最も一般的な名前は RAII です
また、専用のRAIIオブジェクトの外側に何か新しいものを割り当てる必要がある場合に、結果のポインターをラップするために使用されるスマートポインタークラスを調べます。代わりに、ポインタをスマートポインタに渡します。スマートポインタは、たとえば参照カウントによってその有効期間を追跡し、最後の参照が範囲外になったときにデストラクタを呼び出します。標準ライブラリには、単純なスコープベースの管理のためのstd::unique_ptr
と、共有所有権を実装するための参照カウントを行うstd::shared_ptr
があります。
多くのチュートリアルでは、次のようなスニペットを使用してオブジェクトのインスタンス化を示しています...
それで、あなたが発見したのは、ほとんどのチュートリアルがひどいことです。 ;)ほとんどのチュートリアルでは、必要のないときに変数を作成するためにnew/deleteを呼び出したり、割り当ての有効期間を追跡するのに苦労したりするなど、お粗末なC++プラクティスを教えます。
スタック上に物を置くことは、割り当てと自動解放の点では有利かもしれませんが、いくつかの欠点があります。
スタックに巨大なオブジェクトを割り当てたくない場合があります。
ダイナミックディスパッチ!次のコードを検討してください。
#include <iostream>
class A {
public:
virtual void f();
virtual ~A() {}
};
class B : public A {
public:
virtual void f();
};
void A::f() {cout << "A";}
void B::f() {cout << "B";}
int main(void) {
A *a = new B();
a->f();
delete a;
return 0;
}
これは「B」を印刷します。 Stackを使用すると何が起こるかを見てみましょう。
int main(void) {
A a = B();
a.f();
return 0;
}
これは「A」を出力しますが、これはJavaまたは他のオブジェクト指向言語に精通している人には直観的ではないかもしれません。その理由は、B
のインスタンスへのポインターがなくなったためです。代わりに、B
のインスタンスが作成され、タイプa
のA
変数にコピーされます。
特にC++を初めて使用する場合、いくつかのことが直感に反して発生する可能性があります。 Cにはポインタがあり、それだけです。あなたはそれらを使用する方法を知っていて、彼らは常に同じことをします。 C++では、これは当てはまりません。この例でaをメソッドの引数として使用すると、何が起こるか想像してみてください-a
のタイプがA
またはA*
またはA&
(参照による呼び出し)である場合、事態はさらに複雑になり、大きな違いが生じます。多くの組み合わせが可能であり、それらはすべて異なる動作をします。
さて、ポインタを使用する理由は、mallocで割り当てられたCでポインタを使用する理由とまったく同じです。オブジェクトを変数より長く存続させたい場合は!
回避できる場合は、new演算子を使用しないことを強くお勧めします。特に例外を使用する場合。一般に、コンパイラにオブジェクトを解放させる方がはるかに安全です。
私は、&address-of演算子をまったく取得していない人々からこのアンチパターンを見てきました。ポインターを使用して関数を呼び出す必要がある場合は、常にヒープに割り当てて、ポインターを取得します。
void FeedTheDog(Dog* hungryDog);
Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;
Dog goodDog;
FeedTheDog(&goodDog);
ヒープを非常に重要な不動産として扱い、非常に慎重に使用します。基本的な経験則は、stack 可能な限りを使用し、他に方法がない場合は常にヒープを使用することです。オブジェクトをスタックに割り当てることにより、次のような多くの利点を得ることができます。
(1)。例外の場合にスタックの巻き戻しを心配する必要はありません
(2)。ヒープマネージャが必要とする以上の領域を割り当てることによって生じるメモリの断片化について心配する必要はありません。
私が心配する唯一の理由は、Dogがヒープではなくスタックに割り当てられるようになったことです。したがって、Dogのサイズがメガバイトの場合、問題が発生する可能性があります。
新規/削除ルートに進む必要がある場合は、例外に注意してください。このため、auto_ptrまたはブーストスマートポインタータイプのいずれかを使用して、オブジェクトの有効期間を管理する必要があります。
スタックに割り当てることができる場合(ヒープ上)に新しい理由はありません(何らかの理由で小さなスタックがあり、ヒープを使用する場合を除きます)。
ヒープに割り当てる場合は、標準ライブラリのshared_ptr(またはそのバリアントの1つ)を使用することを検討してください。 shared_ptrへのすべての参照が存在しなくなると、削除が処理されます。
オブジェクトを動的に作成することを選択する理由として、他の誰も言及していない追加の理由があります。動的なヒープベースのオブジェクトを使用すると、 ポリモーフィズム を使用できます。