11

私は修士号を取得するためにいくつかのプロジェクトで C を使用してきましたが、それを使用して実稼働ソフトウェアを構築したことはありません。(.NET と Javascript は私のパンとバターです。) 明らかに、 C ではfree()メモリの必要性malloc()が重要です。1 つのルーチンで両方を実行できれば、これは問題ありません。しかし、プログラムが成長し、構造が深まるにつれて、何がどこで何が行われ、何が解放に適しているかを追跡することmallocはますます難しくなっています。

私はインターウェブを見回しましたが、これに関するいくつかの一般的な推奨事項しか見つかりませんでした. 私が疑うのは、長年の C コーダーの中には、このプロセスを単純化し、目の前の悪を維持するために独自のパターンとプラクティスを考え出した人がいるということです。

では、動的割り当てがメモリ リークにならないように C プログラムをどのように構築することをお勧めしますか?

4

5 に答える 5

8

契約による設計。すべての関数コメントがそのメモリの衛生状態について明示的であることを確認してください。つまり、mallocであり、割り当てられたものを解放する責任があるかどうか、渡されたものの所有権を取得するかどうかを確認してください。関数に一貫性を持たせてください。

たとえば、ヘッダーファイルには次のようなものが含まれている可能性があります。

/* Sets up a new FooBar context with the given frobnication level.
 * The new context will be allocated and stored in *rv; 
 * call destroy_foobar to clean it up. 
 * Returns 0 for success, or a negative errno value if something went wrong. */
int create_foobar(struct foobar** rv, int frobnication_level);

/* Tidies up and tears down a FooBar context. ctx will be zeroed and freed. */
void destroy_foobar(struct foobar* ctx);

Valgrindを使用するためのアドバイスを心から支持します。これは、メモリリークや無効なメモリアクセスを追跡するための本当に素晴らしいツールです。Linuxで実行していない場合は、Electric Fenceも同様のツールですが、機能は劣ります。

于 2010-05-12T09:43:09.963 に答える
5

大規模なプロジェクトでは、「プール」手法がよく使用されます。この場合、各割り当てはプールに関連付けられ、プールが関連付けられると自動的に解放されます。これは、単一の一時プールで複雑な処理を実行でき、終了時に一気に解放できる場合に非常に便利です。サブプールは通常可能です。次のようなパターンがよく見られます。

void process_all_items(void *items, int num_items, pool *p)
{
    pool *sp = allocate_subpool(p);
    int i;

    for (i = 0; i < num_items; i++)
    {
        // perform lots of work using sp

        clear_pool(sp);  /* Clear the subpool for each iteration */
    }
}

文字列操作で物事がずっと簡単になります。文字列関数は、戻り値でもある戻り値を割り当てるプール引数を取ります。

欠点は次のとおりです。

  • プールがクリアまたは解放されるまで待機する必要があるため、オブジェクトの割り当てられた有効期間は少し長くなる場合があります。
  • 追加のプール引数を関数に渡すことになります(関数が必要な割り当てを行うための場所)。
于 2010-05-12T10:59:50.607 に答える
4

それは絶対確実ではありません(しかし、それはおそらくCで予想されます)、そして多くの既存のコードで行うのは難しいかもしれませんが、コードを明確に文書化し、割り当てられたメモリの所有者と責任者を常に正確に述べると役立ちますそれを解放するために(そしてどのアロケータ/デアロケータで)。gotoまた、重要なリソース割り当て機能に対して、単一エントリ/単一出口のイディオムを適用するために使用することを恐れないでください。

于 2010-05-12T09:35:56.750 に答える
3

Valgrindは、メモリ管理を健全に保つのに非常に役立つことがわかりました。割り当てられていないメモリにアクセスする場所と、メモリの割り当てを解除するのを忘れている場所 (および多くのもの) を教えてくれます。

C でメモリ管理を行う高レベルの方法もあります。たとえば、メモリ プールを使用します (たとえば、Apache APRを参照してください)。

于 2010-05-12T08:10:59.927 に答える
2

各タイプのアロケータとデアロケータを抽象化します。与えられた型定義

typedef struct foo
{
  int x;
  double y;
  char *z;
} Foo;

アロケータ関数を作成する

Foo *createFoo(int x, double y, char *z)
{
  Foo *newFoo = NULL;
  char *zcpy = copyStr(z);

  if (zcpy)
  {
    newFoo = malloc(sizeof *newFoo);
    if (newFoo)
    {
      newFoo->x = x;
      newFoo->y = y;
      newFoo->z = zcpy;
    }
  }
  return newFoo;
}

コピー機能

Foo *copyFoo(Foo f)
{
  Foo *newFoo = createFoo(f.x, f.y, f.z);
  return newFoo;
}

およびデロケータ関数

void destroyFoo(Foo **f)
{
  deleteStr(&((*f)->z));
  free(*f);
  *f = NULL;
}

次に、メモリの割り当てと文字列の内容のコピーを担当createFoo()する関数を呼び出すことに注意してください。また、失敗して NULL を返す場合は、メモリの割り当てを試みず、NULL を返すcopyStr()ことにも注意してください。同様に、残りの構造体を解放する前に z のメモリを削除する関数を呼び出します。最後に、f の値を NULL に設定します。 copyStr()newFoodestroyFoo()destroyFoo()

ここで重要なのは、メンバー要素にもメモリ管理が必要な場合、アロケーターとデアロケーターが責任を他の関数に委任することです。したがって、型が複雑になるにつれて、次のようにアロケーターを再利用できます。

typedef struct bar
{
  Foo *f;
  Bletch *b;
} Bar;

Bar *createBar(Foo f, Bletch b)
{
  Bar *newBar = NULL;
  Foo *fcpy = copyFoo(f);
  Bletch *bcpy = copyBar(b);

  if (fcpy && bcpy)
  {
    newBar = malloc(sizeof *newBar);
    if (newBar)
    {
      newBar->f = fcpy;
      newBar->b = bcpy;
    }
  }
  else
  {
    free(fcpy);
    free(bcpy);
  }

  return newBar;
}

Bar *copyBar(Bar b)
{
  Bar *newBar = createBar(b.f, b.b);
  return newBar;
}

void destroyBar(Bar **b)
{
  destroyFoo(&((*b)->f));
  destroyBletch(&((*b)->b));
  free(*b);
  *b = NULL;
}

明らかに、この例では、メンバーがコンテナーの外で有効期間を持たないことを前提としています。常にそうであるとは限らず、それに応じてインターフェイスを設計する必要があります。ただし、これにより、何を行う必要があるかがわかります。

これを行うと、オブジェクトのメモリを一貫性のある明確な順序で割り当ておよび割り当て解除できます。これは、メモリ管理における戦いの 80% です。残りの 20% は、すべてのアロケーター呼び出しがデアロケーターによってバランスが取れていることを確認することです。これは非常に難しい部分です。

編集

関数の呼び出しを変更してdelete*、正しい型を渡すようにしました。

于 2010-05-12T15:12:29.923 に答える