web-dev-qa-db-ja.com

これはメモリを解放する良い方法ですか?

struct Fooのインスタンスを解放する関数を以下に示します。

void DestroyFoo(Foo* foo)
{
    if (foo) free(foo);
}

私の同僚は代わりに次のことを提案しました:

void DestroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL; // prevents future concurrency problems
    memset(tmpFoo, 0, sizeof(Foo));  // problems show up immediately if referred to free memory
    free(tmpFoo);
}

解放後にポインタをNULLに設定する方が良いと思いますが、以下についてはわかりません。

  1. ポインタを一時的なものに割り当てる必要がありますか?並行性と共有メモリの点で役立ちますか?

  2. ブロック全体を0に設定してプログラムを強制的にクラッシュさせること、または少なくとも大幅な不一致の結果を出力することは本当に良い考えですか?

55

ポインタを一時的なものに割り当てる必要がありますか?並行性と共有メモリの点で役立ちますか?

同時実行や共有メモリには何もしません。それは無意味です。

ブロック全体を0に設定してプログラムを強制的にクラッシュさせること、または少なくとも大幅な不一致の結果を出力することは本当に良い考えですか?

いいえ、まったく違います。

同僚から提案された解決策はひどいものです。理由は次のとおりです。

  • ブロック全体を0に設定しても、何も起こりません。誰かがfree()されたブロックを誤って使用しているため、ブロックの値に基づいてそれを知ることはできません。これは、calloc()が返す種類のブロックです。そのため、新しく割り当てられたメモリ(calloc()またはmalloc()+memset())か、以前にコードによってfree()されたメモリかどうかを知ることは不可能です。どちらかと言えば、free()されているメモリのすべてのブロックをゼロにすることは、プログラムにとって余分な作業です。

  • free(NULL);は明確に定義されており、何も実行されないため、if(ptr) {free(ptr);}if条件は何も実行しません。

  • free(NULL);は何もしないので、ポインタをNULLに設定すると、実際にはそのバグが非表示になります。これは、一部の関数が実際にすでにfree()でfree()を呼び出しているためです。 edポインター、彼らはそれを知りません。

  • ほとんどのユーザー関数は、最初にNULLチェックがあり、NULLをエラー条件として渡すことを考慮しない場合があります。

_void do_some_work(void *ptr) {
    if (!ptr) {
        return; 
    }

   /*Do something with ptr here */
}
_

したがって、これらすべての追加のチェックとゼロ調整により、実際には何も改善されなかったにもかかわらず、「堅牢性」の偽の感覚が得られます。ある問題を別の問題に置き換えるだけで、パフォーマンスとコードの膨張という追加のコストが発生しました。

したがって、ラッパー関数なしでfree(ptr);を呼び出すだけで、シンプルかつ堅牢になります(ほとんどのmalloc()実装は、ダブルフリーですぐにクラッシュします。これはgoodのことです)。

「誤って」free()を2回以上呼び出す簡単な方法はありません。割り当てられたすべてのメモリを追跡し、適切にfree()するのはプログラマの責任です。誰かがこれを処理するのが難しいと思うなら、Cはおそらく彼らにとって適切な言語ではありません。

67
P.P.

同僚が示唆することは、関数が2回呼び出された場合にコードを「より安全」にすることです(sleskeコメントを参照してください...「より安全」はすべての人にとって同じではないかもしれないので... ;-)。

あなたのコードでは、これはおそらくクラッシュします:

_Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
DestroyFoo(foo); // will call free on memory already freed
_

同僚のバージョンのコードでは、これはクラッシュしません。

_Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(&foo);
DestroyFoo(&foo); // will have no effect
_

さて、この特定のシナリオでは、_tmpFoo = 0;_(DestroyFoo内)で十分です。 memset(tmpFoo, 0, sizeof(Foo));は、メモリが解放された後にFooが誤ってアクセスされる可能性がある追加の属性を持っている場合のクラッシュを防ぎます。

だから私はそう言います、そうするのは良い習慣かもしれません....しかし、それは一種の悪い習慣を持っている開発者に対するセキュリティです(なぜならDestroyFooを呼び出す理由は絶対にないからです再割り当てせずに2回) ...最後に、DestroyFooを「より安全」にしますが、速度を遅くします(それが悪い使い方を防ぐために多くのことを行います)。

9
jpo38

2番目のソリューションは、過度に設計されているようです。もちろん、状況によっては安全な場合もありますが、オーバーヘッドと複雑さが大きすぎます。

安全な側になりたい場合は、メモリを解放した後にポインタをNULLに設定する必要があります。これは常に良い習慣です。

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
foo = NULL;

さらに、free()を呼び出す前にポインタがNULLかどうかを人々が確認している理由がわかりません。 free()があなたのために仕事をしてくれるので、これは必要ありません。

Free()はメモリをクリアしないため、メモリを0(または他の何か)に設定することは、いくつかの場合にのみ良い習慣です。再利用できるように、メモリの領域に空き領域をマークするだけです。メモリをクリアして、誰もそれを読み取れないようにする場合は、手動でクリーニングする必要があります。しかし、これはかなり重い操作であり、すべてのメモリを解放するためにこれを使用してはいけない理由です。ほとんどの場合、クリアせずに解放するだけで十分であり、不要な操作を行うためにパフォーマンスを犠牲にする必要はありません。

4
codewarrior
void destroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL;
    memset(tmpFoo, 0, sizeof(Foo));
    free(tmpFoo);
}

あなたの同僚のコードは悪いです

  • fooNULLの場合、クラッシュします
  • 追加の変数を作成しても意味がありません
  • 値をゼロに設定しても意味がありません
  • 構造体を解放する必要があるものが含まれている場合、構造体を直接解放しても機能しません

あなたの同僚が考えている可能性があるのは、このユースケースだと思います

Foo* a = NULL;
Foo* b = createFoo();

destroyFoo(NULL);
destroyFoo(&a);
destroyFoo(&b);

その場合、このようになります。 ここでお試しください

void destroyFoo(Foo** foo)
{
    if (!foo || !(*foo)) return;
    free(*foo);
    *foo = NULL;
}

最初にFooを確認する必要があります。次のようになっているとしましょう

struct Foo
{
    // variables
    int number;
    char character;

    // array of float
    int arrSize;
    float* arr;

    // pointer to another instance
    Foo* myTwin;
};

それをどのように破壊するかを定義するために、まずそれをどのように作成するかを定義しましょう

Foo* createFoo (int arrSize, Foo* twin)
{
    Foo* t = (Foo*) malloc(sizeof(Foo));

    // initialize with default values
    t->number = 1;
    t->character = '1';

    // initialize the array
    t->arrSize = (arrSize>0?arrSize:10);
    t->arr = (float*) malloc(sizeof(float) * t->arrSize);

    // a Foo is a twin with only one other Foo
    t->myTwin = twin;
    if(twin) twin->myTwin = t;

    return t;
}

これで、作成関数に対抗する破棄関数を作成できます

Foo* destroyFoo (Foo* foo)
{
    if (foo)
    {
        // we allocated the array, so we have to free it
        free(t->arr);

        // to avoid broken pointer, we need to nullify the twin pointer
        if(t->myTwin) t->myTwin->myTwin = NULL;
    }

    free(foo);

    return NULL;
}

テスト ここでお試しください

int main ()
{
    Foo* a = createFoo (2, NULL);
    Foo* b = createFoo (4, a);

    a = destroyFoo(a);
    b = destroyFoo(b);

    printf("success");
    return 0;
}
1
Khaled.K

残念ながら、このアイデアは機能していません。

意図的にダブルフリーをキャッチすることであった場合、次のようなケースは対象外です。

次のコードを想定します。

_Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
free (ptr_1);
free (ptr_2); /* This is a bug */
_

代わりに書くことを提案します:

_Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
DestroyFoo (&ptr_1);
DestroyFoo (&ptr_2); /* This is still a bug */
_

問題は、_ptr_2_がNULLにリセットされておらず、すでに解放されているメモリをポイントしているため、DestroyFoo()への2番目の呼び出しが依然としてクラッシュすることです。

0
Marc Alff