51

NULLそれらを解放した後にポインタを設定する必要がある理由は2つあるようです。

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

短い: に設定されている場合、誤って 2 回目の呼び出しfree()を行ってもクラッシュしませんNULL

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

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

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

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

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

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

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

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

質問

これは、解放後にやみくもにポインターを設定することに対する投稿NULLです。

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

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

4

10 に答える 10

27

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

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

私自身のコードでは、free() のラッパーである独自の関数のみを呼び出します。ポインターへのポインターを取り、メモリを解放した後にポインターを null にします。また、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()。これは、 がassert()ヌル ポインターについて文句を言うためです。ダブルフリーの試みを検出するためにそのアサートを持っていますが、実際に多くのバグをキャッチしたとは思いません。アサートを省略したい場合は、チェックを省略して常に呼び出すことができますFreeAnything()。アサート以外に、null ポインターを解放しようとしても悪いことは何も起こりませんFreeAnything()。これは、ポインターをチェックし、既に null である場合に返すだけだからです。

私の実際の関数名はかなり簡潔ですが、この例では自己文書化された名前を選択しようとしました。また、私の実際のコードでは、0xDC呼び出す前にバッファーに値を入力するデバッグ専用コードがあるfree()ため、同じメモリ (null にされないもの) への追加のポインターがある場合、データがそれが指しているのは偽のデータです。DEBUG_ONLY()非デバッグ ビルドでは何もコンパイルされないマクロがあります。FILL()構造体に対して aを実行するマクロ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
于 2009-12-10T08:53:40.930 に答える
9

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

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

3 番目のケースでは、ポインターを NULL に設定します。それは特にあなたがそれを解放しているからではなく、それが何であれオプションであるためです。したがって、もちろん 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すると、ダブルフリーのバグが目立たなくなります。ポインターを NULL にしても、2 番目の解放はすぐに害を及ぼすことはありませんが、長期的には間違っています (オブジェクトのライフサイクルが壊れているという事実を裏切るため)。それらを解放するときに非 null であると断言できますが、その結果、オプションの値を保持する構造体を解放する次のコードが生成されます。

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

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

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

私は、ポインターが使用可能なままであると思われる場合は、ポインターを NULL にします。使用できなくなった場合は、NULL などの潜在的に意味のある値を入れて、誤って使用できないようにしないことをお勧めします。ページ フォールトを引き起こしたい場合は、プラットフォームに依存する値を使用します。この値は dereferancable ではありませんが、コードの残りの部分では特別な「すべてが問題なく適切な」値として扱われません。

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

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

于 2009-12-10T13:15:10.823 に答える
4

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

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

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

于 2009-12-10T08:53:12.880 に答える
3

答えは、(1) プロジェクトの規模、(2) コードの期待寿命、(3) チームの規模によって異なります。存続期間が短い小さなプロジェクトでは、ポインターを NULL に設定するのをスキップして、一緒にデバッグすることができます。

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

于 2009-12-10T09:05:45.317 に答える
2

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

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

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

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

于 2012-08-24T09:08:55.913 に答える
1

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

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

  • 常にポインタを初期化します - それらを NULL または有効なアドレスに設定します
  • free() を呼び出した後、ポインターを NULL に設定します。
  • 逆参照する前に、NULL になる可能性のあるポインターが実際に NULL であることを確認してください。
于 2009-12-10T08:46:51.303 に答える
1

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

DRY を中心にコードを整理する - 繰り返さないでください。関連するものは一緒に保管してください。1つのことだけを行い、それをうまく行います。リソースを割り当てる「モジュール」は、リソースを解放する責任があり、そのための関数を提供して、ポインターも管理する必要があります。特定のリソースについては、割り当てられる場所と解放される場所が 1 つずつあり、どちらも近くにあります。

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

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


    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 つの大きな怪物に混ざり合うのではなく、それが属するモジュールに含まれるようになりました。

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

于 2009-12-10T10:39:19.733 に答える
1

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
于 2009-12-10T08:49:40.910 に答える
0

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

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

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

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

于 2009-12-10T08:50:35.230 に答える
0

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

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

于 2009-12-10T08:53:48.347 に答える