同様に、ポインタを削除する前にNULL
をチェックするレガシーコードがよくあります。
if (NULL != pSomeObject)
{
delete pSomeObject;
pSomeObject = NULL;
}
削除する前にNULL
ポインターを確認する理由はありますか?後でポインタをNULL
に設定する理由は何ですか?
Nullポインタを削除することは完全に「安全」です。実質的には何もしないことになります。
削除する前にnullを確認する必要があるのは、nullポインターを削除しようとすると、プログラムにバグがある可能性があるためです。
C++標準では、delete-expression(§8.5.2.5/ 2)でnullポインターを使用することが合法であることを保証しています。ただし、これが解放割り当て関数を呼び出すかどうかはnspecifiedです(operator delete
またはoperator delete[]
;§8.5.2.5/ 7、注)。
デフォルトの割り当て解除関数(つまり、標準ライブラリによって提供される)がnullポインターで呼び出された場合、その呼び出しは効果がありません(6.6.4.4.2/3)。
ただし、割り当て解除関数が標準ライブラリによって提供されない場合に何が起こるかは不明です。つまり、operator delete
(またはoperator delete[]
)をオーバーロードしたときに何が起こるかです。
有能なプログラマーは、OPのコードに示されているように、呼び出しの前ではなく、それに応じてnullポインターinside割り当て解除関数を処理します。同様に、削除後にポインターをnullptr
/NULL
に設定すると、目的が非常に限定されます。 防御的プログラミング の精神でこれを行うことが好きな人もいます:バグの場合にプログラムの動作を少し予測しやすくします:削除後にポインタにアクセスすると、nullポインタアクセスではなく、ランダムなメモリ位置へのアクセス。どちらの操作も未定義の動作ですが、ヌルポインターアクセスの動作は実際にははるかに予測可能です(ほとんどの場合、メモリの破損ではなく直接のクラッシュが発生します)。メモリ破損は特にデバッグが難しいため、削除されたポインターをリセットするとデバッグが容易になります。
—もちろん、これは原因(つまり、バグ)ではなく症状を扱います。 リセットポインターをコードのにおいとして扱う必要があります。クリーンで最新のC++コードにより、メモリの所有権が明確になり、静的にチェックされます(スマートポインターまたは同等のメカニズムを使用)。したがって、この状況を避けることができます。
operator delete
の説明:operator delete
は、(その名前にもかかわらず)他の関数と同様にオーバーロードされる可能性がある関数です。この関数は、一致する引数を持つoperator delete
の呼び出しごとに内部的に呼び出されます。 operator new
についても同様です。
operator new
(およびoperator delete
も)のオーバーロードは、メモリの割り当て方法を正確に制御したい場合に意味があります。これを行うことはそれほど難しいことではありませんが、正しい動作を保証するためにいくつかの予防策を講じる必要があります。 Scott Meyersがこれについて詳しく説明していますEffective C++。
とりあえず、デバッグのためにoperator new
のグローバルバージョンをオーバーロードしたいとしましょう。これを行う前に、次のコードで何が発生するかについての短い注意が1つあります。
klass* pobj = new klass;
// … use pobj.
delete pobj;
ここで実際に何が起こりますか?上記は大まかに次のコードに変換できます。
// 1st step: allocate memory
klass* pobj = static_cast<klass*>(operator new(sizeof(klass)));
// 2nd step: construct object in that memory, using placement new:
new (pobj) klass();
// … use pobj.
// 3rd step: call destructor on pobj:
pobj->~klass();
// 4th step: free memory
operator delete(pobj);
手順2で、少し変な構文でnew
を呼び出していることに注意してください。これは、いわゆるplacement new
への呼び出しであり、アドレスを受け取り、そのアドレスでオブジェクトを構築します。この演算子もオーバーロードされる可能性があります。この場合、クラスklass
のコンストラクターを呼び出すだけです。
さて、さっそくここに、オーバーロードされたバージョンの演算子のコードを示します。
void* operator new(size_t size) {
// See Effective C++, Item 8 for an explanation.
if (size == 0)
size = 1;
cerr << "Allocating " << size << " bytes of memory:";
while (true) {
void* ret = custom_malloc(size);
if (ret != 0) {
cerr << " @ " << ret << endl;
return ret;
}
// Retrieve and call new handler, if available.
new_handler handler = set_new_handler(0);
set_new_handler(handler);
if (handler == 0)
throw bad_alloc();
else
(*handler)();
}
}
void operator delete(void* p) {
cerr << "Freeing pointer @ " << p << "." << endl;
custom_free(p);
}
このコードは、ほとんどの実装と同様に、内部的にmalloc
/free
のカスタム実装を使用します。また、デバッグ出力も作成します。次のコードを検討してください。
int main() {
int* pi = new int(42);
cout << *pi << endl;
delete pi;
}
次の出力が得られました。
Allocating 4 bytes of memory: @ 0x100160
42
Freeing pointer @ 0x100160.
さて、このコードはoperator delete
の標準実装とは根本的に異なる何かを行います:それはnullポインタをテストしませんでした!コンパイラはこれをチェックしないので、上記のコードはコンパイルされますが、ヌルポインターを削除しようとすると、実行時に厄介なエラーが発生する可能性があります。
ただし、前に述べたように、この動作は実際には予期せぬものであり、ライブラリライターはoperator delete
でnullポインタをチェックするよう注意する必要があります。このバージョンは大幅に改善されています:
void operator delete(void* p) {
if (p == 0) return;
cerr << "Freeing pointer @ " << p << "." << endl;
free(p);
}
結論として、operator delete
のずさんな実装ではクライアントコードで明示的なnullチェックが必要になる場合がありますが、これは非標準の動作であり、レガシーサポートでのみ許容されます(ある場合)。
内部的にNULLのチェックを削除します。テストは冗長です
Nullを削除しても何も起こりません。 deleteを呼び出す前にnullを確認する理由はありません。
ポインタがnullであっても、気になる追加情報がある場合は、他の理由でnullをチェックすることをお勧めします。
C++ 03 5.3.5/2によれば、nullポインタを削除しても安全です。以下は標準から引用されています:
どちらの方法でも、deleteのオペランドの値がnullポインターの場合、操作は効果がありません。
PSomeObjectがNULLの場合、削除は何もしません。したがって、いいえ、NULLをチェックする必要はありません。
ナックルヘッドがポインタを使用しようとすることが可能な場合は、ポインタを削除した後でNULLを割り当てることをお勧めします。 NULLポインターの使用は、誰が何を知っているかを示すポインターを使用するよりもわずかに優れています(NULLポインターはクラッシュの原因となり、削除されたメモリへのポインターは使用しない場合があります)。
削除する前にNULLをチェックする理由はありません。削除後にNULLを割り当てる必要があるのは、コードチェックのどこかで、NULLチェックを実行することによってオブジェクトがすでに割り当てられているかどうかを確認する場合です。たとえば、オンデマンドで割り当てられるキャッシュされたデータのようなものです。キャッシュオブジェクトをクリアするときは常に、ポインターにNULLを割り当てて、オブジェクトを割り当てるコードが、割り当てを実行する必要があることを認識します。
以前の開発者が数ミリ秒節約するために「冗長に」コード化したと思います:削除時にポインターをNULLに設定するのは良いことなので、オブジェクトを削除した直後に次のような行を使用できます。
if(pSomeObject1!=NULL) pSomeObject1=NULL;
しかし、削除はとにかくその正確な比較を行っています(NULLの場合は何もしません)。なぜこれを2回行うのですか?現在の値に関係なく、deleteを呼び出した後は常にpSomeObjectをNULLに割り当てることができます。ただし、その値がすでにある場合、これは少し冗長になります。
したがって、私の賭けは、pSomeObject1が削除された後に常にNULLになるようにするために、不要なテストや割り当てのコストを発生させずに、これらの行の作成者になることです。