2

任意のサイズのファイルから行を読み取る関数を備えた単純なライブラリファイルを作成しました。この関数は、スタックに割り当てられたバッファーとサイズを渡すことによって呼び出されますが、行が大きすぎる場合は、特別なヒープに割り当てられたバッファーが初期化され、より大きな行を返すために使用されます。

このヒープ割り当てバッファは、関数スコープで静的と宣言され、もちろん最初にNULLに初期化されます。関数の先頭に、ヒープバッファがnullでないかどうかを確認するためのチェックをいくつか記述しました。この場合、前の行の読み取りが長すぎました。当然、ヒープバッファを解放し、NULLに戻します。次の読み取りでは、スタックに割り当てられたバッファを埋めるだけでよいと考えています(このアプリケーションでも、1MBを超える行が表示されることは非常にまれです)。

私はコードを調べて、注意深く読んだり、いくつかのテストを実行したりして、かなり徹底的にテストしました。私は、次の不変条件が維持されていると合理的に確信しています。

  • スタックバッファが必要なすべてである場合、ヒープバッファは関数の戻り時にnullになります(そしてメモリをリークしません)。
  • ヒープバッファがnullでない場合、必要だったため、次の関数呼び出しで解放されます(必要に応じて、次の行で再利用される可能性があります)。

しかし、潜在的な問題について考えました。ファイルの最後の行が長すぎる場合、関数はおそらく再度呼び出されないため、ヒープバッファーを解放する方法がわかりません。これは関数です。 -スコープ、結局のところ。

だから私の質問は、理想的には関数を再度呼び出さずに、関数スコープの静的ポインタで動的に割り当てられたメモリを解放するにはどうすればよいですか?(そして理想的には、それをグローバル変数にすることなく!)

リクエストに応じて利用可能なコード。(今はアクセスできません。申し訳ありません。質問が十分に一般的で、必要ないように十分に説明されていることを願っていますが、ぜひその概念を私に非難してください!)


編集:関数の使用法についていくつかメモを追加する必要があると思います。

この特定の関数は、ファイルからシリアルに読み取られ、すぐにPOD構造体にコピーされる行の形式で使用されます(構造体ごとに1行)。これらは、ファイルが読み取られるときにヒープ上に作成され、これらの構造体のそれぞれには、ファイルからの行(のクリーンアップされたバージョン)を含むcharポインターがあります。これらが持続するためには、コピーがすでに発生している必要があります。(これは、多くの回答で提起された大きな反論の1つでした。ああ、いや、行をコピーする必要があります。

マルチスレッドに関しては、私が言ったように、これはシリアルに使用されるように設計されています。いいえ、スレッドセーフではありませんが、私は気にしません。

たくさんのご回答ありがとうございます!時間があればもっとよく読みます。fgets現在、私は余分なポインターを渡すか、関数を再設計して、 EOFが表示されたときに、代わりに解放ロジックを構築するだけで、ユーザーがそれについて心配する必要がないようにすることに傾倒しています。

4

8 に答える 8

3

関数を変更できる場合は、関数インターフェイス自体を変更することをお勧めします。デバッグとテストに多くの時間を費やしたことは承知していますが、現在の実装にはいくつかの問題があります。

  • スレッドセーフではありません。
  • ユーザーはデータを制御できないため、後で必要に応じてコピーする必要があります。ほとんどの場合、ed されるバッファーにコピーする必要があります。したがって、関数で をmalloc()選択的に使用することによって得られる利点が無効になります。malloc()
  • 最も重要なのは、あなたが発見したように、最後の長い行に対してユーザーが特別なアクションを実行する必要があることです。

ユーザーは、関数の実装の奇妙さを心配する必要はありません。「そのまま使用」できるはずです。

教育目的で実行している場合を除き、このページを見ることをお勧めします。このページには、「ストリームから任意の長い行を読み取る」実装が 1 つあり、他のそのような実装へのリンクがあります (各実装は他の実装とはわずかに異なるため、お気に入りの一枚がきっと見つかるはずです。)

編集に基づいて、MT セーフは要件ではなく、コピーは常に行われます。したがって、最も明白な設計は次の 2 つのうちの 1 つです。

  • と(必要な場合)char **の組み合わせを使用して、関数が割り当てるバッファーを指す をユーザーに提供させます。実行時はユーザーの責任です。そうすれば、ユーザーはデータの最終的な宛先がどこであってもポインターを渡すことができるため、データを再度コピーする必要はありません。malloc()realloc()free()
  • char *関数によって割り当てられたa を返します。繰り返しますが、それはユーザーの責任free()です。

どちらもほぼ同等です。

現在の実装では、最後の行が非常に長く、改行で終わらない場合は、常に「ファイルの終わりではない」を返すことができます。次に、ユーザーが関数を再度呼び出すと、バッファーを解放できます。個人的には、ファイルの最後まで行かなくても、好きなだけ行を読める機能があればもっと嬉しいです。

于 2010-01-25T23:31:47.627 に答える
1

動的に割り当てられたバッファーを解放することの難しさは別として、別の潜在的な問題があります。スレッドセーフではありません。ライブラリ関数なので、将来マルチスレッド環境で使われる可能性は常にあります。

関連するライブラリ関数を介してバッファを解放するように呼び出し関数を要求する方がおそらく良いでしょう。

于 2010-01-25T23:19:48.300 に答える
1

関数スコープの代わりに、モジュール スコープを与えます (つまり、ファイル スコープですが、静的なので、そのファイルの外では見えません。バッファーを解放する小さな関数を追加しatexit()、プログラムが終了する前に呼び出されることを保証するために使用します。代わりに、don'心配する必要はありません。一度だけ発生し、プログラムの終了時に自動的に解放されるリークは、特に有害ではありません。

でも、そのデザインは災害のレシピのように聞こえると言わざるを得ません. バッファーを解放すると、それがまだ使用されているかどうかを推測する方法さえ事実上ありません。ユーザーは (明らかに) データが返された場所を追跡し、データを動的に割り当てた場合 (およびその場合にのみ)、新しいバッファーにデータをコピーする必要があります。マルチスレッド環境では、内部ポインターをスレッドローカルにして、正しく動作する可能性を少しでも確保する必要があります。ユーザーにとって、関数は 2 つのまったく異なることのいずれかを行う可能性があります。つまり、ユーザーが所有するバッファーを返すか、関数が所有するバッファーを返し、別のバッファーを割り当ててコピーすることによってのみ安全に使用できます。関数が再度呼び出される前に、データを他のバッファーに格納します。

于 2010-01-25T23:25:15.433 に答える
1

標準的な手法を使用してファイルの終わりを示す場合 (つまり、read-line 関数が NULL を返すようにする場合) は、それでも問題ありません。

この場合、最終行が読み取られた後、ファイルの終わりに達したことを示すために NULL を返すことができるように、行読み取り関数をもう一度呼び出す必要があります。この最後の呼び出しで、バッファを解放できます。

于 2010-01-25T23:26:16.367 に答える
1

あなたが選択したインターフェースは、これを解決できない問題にしています:

  • クライアントは、戻り値が静的または動的メモリを指しているかどうかを認識してはなりません。

  • 戻り値は、呼び出し後も存続するメモリを指している必要があります。

  • すべての呼び出しが最後になる可能性があります。

なぜこのリークに悩まされているのかわかりません。結局のところ、クライアントが非常に長い行を読み取り、その行で何かを行い、次の行を読み取る前に大量の計算と割り当てを行うと、未使用のメモリが大量に残ってシステムを詰まらせます。これで問題がなければ (メモリが再利用される前に任意の計算が行われる)、死んだメモリを無期限に保持する意思があると断言できます。

リークに耐えられない場合の最も簡単な方法は、インターフェイスを拡張して、クライアントがメモリの処理を完了したときにクライアントが関数に通知できるようにすることです。(現在、クライアントとの契約では、クライアントが関数を再度呼び出すまでメモリを所有するとされています。その時点で、所有権は関数に戻ります。) もちろん、インターフェイスを変更するということは、次のいずれかを意味します。

  • static新しい関数を追加します。これには、ポインタをコンパイル単位に対してローカルに昇格させる必要があります。または

  • 既存の関数にいくつかの引数を追加する (または引数をオーバーロードする) ことで、「これで記憶は終わりましたが、別の行は必要ありません」という意味の呼び出しができます。

より根本的な変更は、動的に割り当てられたメモリを使用するように関数を書き直すことです。必要に応じて、これまでに読み取られた最大のブロックと同じ大きさになるまでブロックを徐々に拡大します (または、次の 2 の累乗に切り上げます)。実際のケースによっては、この戦略は、大きな静的バッファーを保持するよりも消費するアドレス空間が少なくなる場合があります。

いずれにせよ、このコーナー ケースについて心配する必要があるとは思えません。このケースが重要だと思われる場合は、質問を編集して証拠を示してください。

于 2010-01-26T03:43:17.067 に答える
1

すぐに発生する 2 つの選択肢:

  1. ヒープ割り当てバッファーへのポインターを静的にしますが、ファイル スコープにします。null でないかどうかをチェックする (静的) 関数を追加し、null でない場合は free() します。プログラムの開始時に atexit(free_func) を呼び出します。ここで、free_func は静的関数です。これが行われるいくつかのグローバル セットアップ ルーチン (main() によって呼び出される) を持つことができます。

  2. ご心配なく; プロセスが終了すると、ヒープに割り当てられたメモリが OS によって解放されます。メモリ リークは累積的ではないため、プログラムの寿命が長くても、OOM 例外は発生しません (他のバグがない限り)。

あなたのアプリはマルチスレッドではないと思います。この場合、静的バッファをまったく使用しないか、スレッド ローカル データを使用する必要があります。

于 2010-01-25T23:26:46.797 に答える
0

マークの回答の下にコメントするつもりでしたが、少し窮屈に感じるかもしれません。それでも、この回答は本質的に彼の回答に対するコメントであり、迅速であることに加えて非常に優れていると思います:)。

関数が MT セーフではないだけでなく、スレッドがなくても、関数を正しく使用するためのインターフェイスが複雑になります。呼び出し元は、関数を再度呼び出す前に、前の結果を終了している必要があります。このコードが今から 2 年後も使用されている場合、誰かがそれを正しく使用しようとして頭をかきむしるでしょう...さらに悪いことに、何も考えずに間違って使用することになります。その人はあなたかもしれません...

マークの提案 (呼び出し元にバッファーを解放することを要求する) は、私見で最も合理的です。しかし、長い目で見ればフラグメンテーションを起こさないように信頼mallocしていないfreeか、または静的バッファー ソリューションを好む理由が他にあるかもしれません。この場合、通常の長さの行の静的バッファーを保持し、静的バッファーが現在ビジーかどうかを示すブール値フラグを定義し、次の関数 ( ではなくfree) をバッファーのアドレスで呼び出す必要があることを文書化できます。呼び出し元はそれを使用しなくなりました:

char static_buffer[512];
int buffer_busy;

void free_buffer(char *p)
{
  if (p == static_buffer)
  {
     assert(buffer_busy);
     buffer_busy=0;
  }
  else free(p);
}

char *get_line(...)
{
  char *result;
  if (..short line..)
  {
     result = static_buffer;
     assert(!buffer_busy);
     buffer_busy=1;
  }
  else result = malloc(...);
  ...
  return result;
}

アサーションがトリガーされる唯一の状況は、以前の実装が暗黙のうちに失敗した状況であり、オーバーヘッドは既存のソリューションと比較して非常に低くなります (フラグを切り替えて、呼び出し元にfree_buffer終了時に呼び出すように依頼するだけです。よりきれいです)。特定のトリガーのアサーションが発生した場合get_line、呼び出し元が別のバッファーを要求したときにバッファーを終了できなかったため、動的割り当てが必要だったことを意味します。

注: これはまだ MT セーフではありません。

于 2010-01-25T23:42:25.143 に答える
0

考えられるハックがいくつかありますが、どちらも static 宣言を関数の外に移動する必要があります。なぜそれが問題になるのか想像できません。

GCC 拡張機能を使用して、

static char *buffer;
void use_buffer(size_t n) {
    buffer = realloc(buffer, n);
}
void cleanup_buffer() __attribute__((destructor)) {
    free(buffer);
}

C++ を使用して、

static char *buffer;
static class buffer_guard {
    ~buffer_guard() { free(buffer); }
} my_buffer_guard;

いずれにせよ、私はデザインがあまり好きではありません。C では、通常、呼び出し先によって埋められた場合でも、呼び出し元が使用する必要があるメモリを割り当て/解放する責任があります。

ところで、Glibc の非標準のgetlineと比較してください。静的メモリは使用しません。

于 2010-01-25T23:24:28.900 に答える