web-dev-qa-db-ja.com

ポインターを解放した後、実際にポインターを「NULL」に設定する必要がありますか?

解放後にNULLへのポインターを設定する必要がある理由は2つあるようです。

ポインターを二重に解放する際のクラッシュを回避します。

ショート:free()を2回呼び出しても、誤ってNULLに設定されていてもクラッシュしません。

  • free()をもう一度呼び出す理由がないため、ほとんどの場合、これは論理的なバグを隠します。アプリケーションをクラッシュさせて修正できる方が安全です。

  • 新しいメモリが同じアドレスに割り当てられることがあるため、クラッシュすることは保証されていません。

  • 二重解放は、同じアドレスを指す2つのポインターがある場合にほとんど発生します。

論理エラーもデータ破損につながる可能性があります。

解放されたポインタの再利用を避ける

ショート:malloc()が同じ場所にメモリを割り当てた場合、解放されたポインタにアクセスすると、解放されたポインタがNULL

  • オフセットが十分に大きければ(NULLポインターにアクセスするときにプログラムがクラッシュするという保証はありません(someStruct->lastMembertheArray[someBigNumber])。クラッシュする代わりに、データが破損します。

  • ポインターをNULLに設定しても、同じポインター値を持つ別のポインターを持つ問題を解決できません。

質問

解放後にNULLへのポインタを盲目的に設定することに対する投稿 です。

  • デバッグが難しいのはどれですか?
  • 両方をキャッチする可能性はありますか?
  • そのようなバグがクラッシュする代わりにデータ破損につながる可能性はどのくらいありますか?

この質問を自由に展開してください。

48
Georg Schölly

2番目の方法はもっと重要です。解放されたポインターを再使用すると、微妙なエラーになる可能性があります。コードは正常に機能し続けますが、明確な理由もなくクラッシュします。これは、再利用されたポインターが指しているように見える一見無関係なコードがメモリに書き込んだためです。

私はかつて本当に他の誰かが書いたバグのあるプログラムに取り組まなければなりませんでした。私の本能から、バグの多くは、メモリを解放した後もポインタを使い続けようとするずさんな試みに関連していると言われました。メモリを解放した後、ポインターをNULLに設定するようにコードを変更し、bam、nullポインター例外が発生し始めました。すべてのNULLポインター例外を修正した後、コードは突然muchより安定しました。

私自身のコードでは、free()のラッパーである独自の関数のみを呼び出します。ポインターへのポインターを取り、メモリーを解放した後、ポインターをヌルにします。そして、freeを呼び出す前にAssert(p != NULL);を呼び出すため、同じポインターを二重に解放しようとする試みをキャッチします。

私のコードも他のことを行います。(DEBUGビルドのみ)割り当てた直後に明白な値でメモリを埋め、ポインタのコピーがある場合にfree()を呼び出す直前に同じことを行います。など 詳細はこちら

編集:リクエストごとに、ここにサンプルコードがあります。

_void
FreeAnything(void **pp)
{
    void *p;

    AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
    if (!pp)
        return;

    p = *pp;
    AssertWithMessage(p != NULL, "attempt to free a null pointer");
    if (!p)
        return;

    free(p);
    *pp = NULL;
}


// FOO is a typedef for a struct type
void
FreeInstanceOfFoo(FOO **pp)
{
    FOO *p;

    AssertWithMessage(pp != NULL, "need pointer-to-pointer, got null value");
    if (!pp)
        return;

    p = *pp;
    AssertWithMessage(p != NULL, "attempt to free a null FOO pointer");
    if (!p)
        return;

    AssertWithMessage(p->signature == FOO_SIG, "bad signature... is this really a FOO instance?");

    // free resources held by FOO instance
    if (p->storage_buffer)
        FreeAnything(&p->storage_buffer);
    if (p->other_resource)
        FreeAnything(&p->other_resource);

    // free FOO instance itself
    free(p);
    *pp = NULL;
}
_

コメント:

2番目の関数では、2つのリソースポインターをチェックして、それらがnullでないかどうかを確認し、FreeAnything()を呼び出す必要があることがわかります。これは、NULLポインターについて文句を言うassert()が原因です。ダブルフリーの試みを検出するためにその主張を持っていますが、実際に多くのバグをキャッチしたとは思いません。アサートを省略したい場合は、チェックを省略して、常にFreeAnything()を呼び出すことができます。アサート以外では、FreeAnything()を使用してNULLポインターを解放しようとしても、ポインターをチェックし、既にNULLである場合に戻るだけなので、何も悪いことは起こりません。

私の実際の関数名はかなり簡潔ですが、この例では自己文書化された名前を選択しようとしました。また、実際のコードには、free()を呼び出す前に値_0xDC_でバッファを埋めるデバッグ専用コードがあります。これにより、同じメモリへの追加のポインタ(無効にされる)、それが指しているデータが偽のデータであることが本当に明らかになります。 DEBUG_ONLY()というマクロがあります。これは、デバッグ以外のビルドでは何にもコンパイルされません。および構造体でFILL()を実行するマクロsizeof()これら2つの機能は、sizeof(FOO)またはsizeof(*pfoo)でも同様に機能します。 FILL()マクロは次のとおりです。

_#define FILL(p, b) \
    (memset((p), b, sizeof(*(p)))
_

呼び出し前にFILL()を使用して_0xDC_値を設定する例を次に示します。

_if (p->storage_buffer)
{
    DEBUG_ONLY(FILL(pfoo->storage_buffer, 0xDC);)
    FreeAnything(&p->storage_buffer);
}
_

これの使用例:

_PFOO pfoo = ConstructNewInstanceOfFoo(arg0, arg1, arg2);
DoSomethingWithFooInstance(pfoo);
FreeInstanceOfFoo(&pfoo);
assert(pfoo == NULL); // FreeInstanceOfFoo() nulled the pointer so this never fires
_
25
steveha

私はこれをしません。私がやった方が対処しやすいバグを特に覚えていません。しかし、それは本当にコードの書き方に依存します。私が何かを解放する状況はおよそ3つあります。

  • それを保持しているポインタが範囲外に出ようとしているとき、または範囲外に出ようとしているまたは解放されようとしているオブジェクトの一部であるとき。
  • オブジェクトを新しいオブジェクトに置き換えるとき(たとえば、再割り当ての場合)。
  • オプションで存在するオブジェクトをリリースするとき。

3番目のケースでは、ポインターをNULLに設定します。それはあなたがそれを解放しているからではなく、what-it-isがオプションだからです。したがって、もちろんNULLは「持っていない」という意味の特別な値です。

最初の2つのケースでは、ポインターをNULLに設定することは、特に目的のない忙しい作業のように思えます。

int doSomework() {
    char *working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // wtf? In case someone has a reference to my stack?
    return result;
}

int doSomework2() {
    char * const working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // doesn't even compile, bad luck
    return result;
}

void freeTree(node_type *node) {
    for (int i = 0; i < node->numchildren; ++i) {
        freeTree(node->children[i]);
        node->children[i] = NULL; // stop wasting my time with this rubbish
    }
    free(node->children);
    node->children = NULL; // who even still has a pointer to node?

    // Should we do node->numchildren = 0 too, to keep
    // our non-existent struct in a consistent state?
    // After all, numchildren could be big enough
    // to make NULL[numchildren-1] dereferencable,
    // in which case we won't get our vital crash.

    // But if we do set numchildren = 0, then we won't
    // catch people iterating over our children after we're freed,
    // because they won't ever dereference children.

    // Apparently we're doomed. Maybe we should just not use
    // objects after they're freed? Seems extreme!
    free(node);
}

int replace(type **thing, size_t size) {
    type *newthing = copyAndExpand(*thing, size);
    if (newthing == NULL) return -1;
    free(*thing);
    *thing = NULL; // seriously? Always NULL after freeing?
    *thing = newthing;
    return 0;
}

解放後にポインターを逆参照しようとするバグがある場合、NULLポインターを使用するとポインターがより明確になります。ポインターをNULLにしない場合、おそらく間接参照はすぐに害を及ぼすことはありませんが、長期的には間違っています。

また、ポインターをNULLにするobscuresすると、ダブルフリーになるバグが発生します。 2番目のfreeは、ポインターをNULLにするとすぐに害はありませんが、長い目で見れば間違っています(オブジェクトのライフサイクルが壊れているという事実を裏切るからです)。物を解放するとき、物事は非ヌルであると断言できますが、その結果、オプションの値を保持する構造体を解放するための次のコードが生成されます。

if (thing->cached != NULL) {
    assert(thing->cached != NULL);
    free(thing->cached);
    thing->cached = NULL;
}
free(thing);

そのコードがあなたに伝えることは、あなたが行き過ぎているということです。そのはず:

free(thing->cached);
free(thing);

想定が使用可能なままの場合は、ポインターをNULLにします。 NULLのように潜在的に意味のある値を入力することにより、それが使用できなくなった場合、誤って表示されないようにすることが最善です。ページフォールトを発生させる場合は、参照不可ではないが、コードの残りの部分では特別な「すべてが正常でダンディな」値として扱われないプラットフォーム依存の値を使用します。

free(thing->cached);
thing->cached = (void*)(0xFEFEFEFE);

システム上でそのような定数が見つからない場合は、読み取り不可能なページや書き込み不可能なページを割り当てて、そのアドレスを使用できる場合があります。

8
Steve Jessop

ポインタをNULLに設定しないと、アプリケーションが未定義の状態で実行され続け、完全に無関係なポイントで後でクラッシュする可能性がそれほど高くありません。次に、存在しないエラーのデバッグに多くの時間を費やしてから、それが以前のメモリ破損であることを確認します。

ポインターをNULLに設定したのは、NULLに設定しなかった場合よりも早くエラーの正しい箇所に到達する可能性が高いためです。もう一度メモリを解放するという論理的なエラーはまだ考えられており、オフセットが十分に大きいヌルポインターアクセスでアプリケーションがクラッシュしないというエラーは、不可能ではないが完全にアカデミックな意見です。

結論:ポインターをNULLに設定します。

3
Kosi2801

答えは、(1)プロジェクトのサイズ、(2)コードの予想寿命、(3)チームのサイズによって異なります。ライフタイムが短い小さなプロジェクトでは、ポインタをNULLに設定することをスキップして、そのままデバッグできます。

大規模で長寿命のプロジェクトでは、ポインターをNULLに設定する正当な理由があります。(1)防御的なプログラミングは常に適切です。あなたのコードは大丈夫かもしれませんが、隣の初心者はまだポインターで苦労しているかもしれません(2)私の個人的な信念は、すべての変数は常に有効な値のみを含むべきだということです。削除/解放後、ポインターは有効な値ではなくなったため、その変数から削除する必要があります。 NULL(常に有効な唯一のポインター値)をNULLに置き換えることは良いステップです。 (3)コードが死ぬことはありません。それは常に再利用され、そしてあなたがそれを書いた時に想像もしなかった方法でしばしば使われます。コードセグメントがC++コンテキストでコンパイルされ、デストラクタまたはデストラクタによって呼び出されるメソッドに移動する可能性があります。破壊される過程にある仮想メソッドとオブジェクトの相互作用は、非常に経験豊富なプログラマーにとっても微妙なtrapです。 (4)コードがマルチスレッドコンテキストで使用されることになった場合、他のスレッドがその変数を読み取ってアクセスしようとする可能性があります。このようなコンテキストは、レガシーコードがラップされてWebサーバーで再利用されるときにしばしば発生します。 (偏執的な観点から)メモリを解放するさらに良い方法は、(1)ポインターをローカル変数にコピーし、(2)元の変数をNULLに設定し、(3)ローカル変数を削除/解放することです。

3
Carsten Kuckuk

ポインターを再利用する場合は、使用しているオブジェクトがヒープから解放されていなくても、使用後に0(NULL)に戻す必要があります。これにより、有効なチェックが可能になります。 if(p){//何かをする}のようなNULL。また、ポインターが指しているアドレスのオブジェクトを解放するからといって、deleteキーワードまたはfree関数を呼び出した後にポインターが0に設定されるわけではありません。

ポインターが1回使用され、それがローカルになるスコープの一部である場合、NULLに設定する必要はありません。関数が戻った後にスタックから破棄されるためです。

ポインターがメンバー(構造体またはクラス)である場合、NULLに対する有効なチェックのためにダブルポインター上のオブジェクトを再度解放した後、ポインターをNULLに設定する必要があります。

これを行うと、「0xcdcd ...」などの無効なポインターによる頭痛の種を軽減できます。そのため、ポインターが0の場合、ポインターがアドレスを指していないことがわかり、オブジェクトがヒープから解放されていることを確認できます。

2
bvrwoo_3376

未定義の振る舞いを扱うため、どちらも非常に重要です。あなたのプログラムで未定義の振る舞いをする方法を残してはいけません。どちらもクラッシュ、データの破損、微妙なバグ、その他の悪い結果につながる可能性があります。

両方をデバッグするのは非常に困難です。特に複雑なデータ構造の場合、両方を確実に回避することはできません。とにかく、次のルールに従えば、はるかに良くなります。

  • 常にポインターを初期化する-NULLまたは有効なアドレスに設定する
  • free()を呼び出した後、ポインターをNULLに設定します
  • 参照を解除する前に、実際にNULLである可能性があるNULLの可能性のあるポインターを確認してください。
1
sharptooth

C++では、独自のスマートポインターを実装する(または既存の実装から派生する)ことと、次のようなものを実装することの両方でキャッチできます。

void release() {
    assert(m_pt!=NULL);
    T* pt = m_pt;
    m_pt = NULL;
    free(pt);
}

T* operator->() {
    assert(m_pt!=NULL);
    return m_pt;
}

あるいは、Cでは、少なくとも同じ効果の2つのマクロを提供できます。

#define SAFE_FREE(pt) \
    assert(pt!=NULL); \
    free(pt); \
    pt = NULL;

#define SAFE_PTR(pt) assert(pt!=NULL); pt
1
Sebastian

これらの問題は、ほとんどの場合、はるかに深い問題の症状にすぎません。これは、取得およびそれ以降のリリースを必要とするすべてのリソースで発生する可能性があります。メモリ、ファイル、データベース、ネットワーク接続など。核となる問題は、コード構造が欠落し、ランダムなmallocがスローされ、コードベース全体が解放されるため、リソース割り当ての追跡ができなくなることです。

DRY-自分自身を繰り返さないでください。関連することをまとめてください。1つのことだけを行い、それをうまく行ってください。リソースを割り当てる「モジュール」がそれを解放し、特定のリソースについては、割り当てられた場所と解放された場所が正確に1か所になり、両方が近くになります。

文字列を部分文字列に分割するとします。 malloc()を直接使用して、関数はすべてを処理する必要があります。文字列の分析、適切な量のメモリの割り当て、そこに部分文字列のコピー、およびand。関数を十分に複雑にします。リソースを追跡できなくなるかどうかは問題ではありません。

最初のモジュールは、実際のメモリ割り当てを処理します。


    void *MemoryAlloc (size_t size)
    void  MemoryFree (void *ptr)

コードベース全体でmalloc()およびfree()が呼び出される唯一の場所があります。

次に、文字列を割り当てる必要があります。


    StringAlloc (char **str, size_t len)
    StringFree (char **str)

Len + 1が必要であり、解放時にポインターがNULLに設定されることに注意してください。サブストリングをコピーする別の関数を提供します。


    StringCopyPart (char **dst, const char *src, size_t index, size_t len)

Indexとlenがsrc文字列内にある場合は注意し、必要に応じて変更します。 dstに対してStringAllocを呼び出し、dstが正しく終了するようにします。

これで、分割関数を作成できます。これで、低レベルの詳細を気にする必要がなくなり、文字列を分析して部分文字列を取得するだけです。ロジックの大部分は、1つの大きな怪物に混ざり合うのではなく、属するモジュール内にあります。

もちろん、このソリューションには独自の問題があります。抽象化レイヤーを提供し、各レイヤーは他の問題を解決すると同時に、独自のセットを備えています。

1
Secure

2つの問題のうち、回避しようとしている「より重要な」部分は実際にはありません。信頼できるソフトウェアを作成する場合は、本当に両方を避ける必要があります。また、上記のいずれかがデータ破損を引き起こし、ウェブサーバーがこれらの行に沿ってpwnedされ、他の楽しみを持っている可能性が非常に高いです。

また、覚えておくべき重要なステップがもう1つあります。ポインタを解放してからNULLに設定することは、作業の半分にすぎません。理想的には、このイディオムを使用している場合、ポインターアクセスも次のようにラップする必要があります。

if (ptr)
  memcpy(ptr->stuff, foo, 3);

ポインタ自体をNULLに設定するだけで、プログラムは不適切な場所でのみクラッシュします。これは、データを静かに破損するよりもおそらく良いでしょうが、それでもあなたが望むものではありません。

0
Timo Geusch

NULLポインターにアクセスしたときにプログラムがクラッシュする保証はありません。

たぶん標準ではないかもしれませんが、クラッシュや例外を引き起こす不正な操作として定義されていない実装を見つけるのは難しいでしょう(ランタイム環境に応じて)。

0
Anon.