驚くほど単純/愚か/基本的な質問ですが、私にはわかりません。関数の最初の部分では長さがわからないC文字列を関数のユーザーに返したいとします。当初は長さの上限しか配置できず、加工によってはサイズが縮む場合がございます。
問題は、十分なヒープスペース(上限)を割り当ててから、処理中にそれよりもかなり短い文字列を終了することで何か問題があるのでしょうか。つまり、割り当てられたメモリの中央に '\ 0'を挿入した場合、(a。)free()
は引き続き正常に機能し、(b。) '\ 0'の後のスペースは重要ではなくなりますか? '\ 0'が追加されると、メモリは返されるだけですか、それともfree()
が呼び出されるまでスペースを占有していますか? mallocを呼び出す前に必要なスペースを計算する前もってのプログラミング時間を節約するために、このぶら下がっているスペースをそこに残すことは、一般的に悪いプログラミングスタイルですか?
これにコンテキストを与えるために、次のように連続する重複を削除したいとします。
「Hello oOOOo !!」と入力します->「Helo oOo!」を出力します
...そして、以下のコードは、操作の結果のサイズをどのように事前計算して、ヒープサイズを正しく取得するために処理を2回効率的に実行するかを示しています。
char* RemoveChains(const char* str)
{
if (str == NULL) {
return NULL;
}
if (strlen(str) == 0) {
char* outstr = (char*)malloc(1);
*outstr = '\0';
return outstr;
}
const char* original = str; // for reuse
char prev = *str++; // [prev][str][str+1]...
unsigned int outlen = 1; // first char auto-counted
// Determine length necessary by mimicking processing
while (*str) {
if (*str != prev) { // new char encountered
++outlen;
prev = *str; // restart chain
}
++str; // step pointer along input
}
// Declare new string to be perfect size
char* outstr = (char*)malloc(outlen + 1);
outstr[outlen] = '\0';
outstr[0] = original[0];
outlen = 1;
// Construct output
prev = *original++;
while (*original) {
if (*original != prev) {
outstr[outlen++] = *original;
prev = *original;
}
++original;
}
return outstr;
}
割り当てられたメモリの中央に「\ 0」を挿入すると、
(a。)free()は引き続き適切に機能し、
はい。
(b。) '\ 0'の後のスペースは重要ではなくなりますか? '\ 0'が追加されたら、メモリが返されるだけですか、それともfree()が呼び出されるまでメモリを占有し続けますか?
依存します。多くの場合、大量のヒープスペースを割り当てると、システムは最初に仮想アドレススペースを割り当てます。ページに書き込むと、実際の物理メモリが割り当てられます(後で、OSに仮想メモリがある場合、ディスクにスワップアウトされる可能性があります)サポート)。有名なことに、仮想アドレス空間の無駄な割り当てと実際の物理/スワップメモリとのこの違いにより、スパース配列は、このようなOSでメモリ効率を合理的にすることができます。
さて、この仮想アドレッシングとページングの粒度はメモリページサイズにあります-それは4k、8k、16kかもしれません...?ほとんどのOSには、ページサイズを調べるために呼び出すことができる関数があります。したがって、多くの小さな割り当てを行っている場合は、ページサイズに切り上げるのは無駄であり、実際に使用する必要があるメモリの量に比べてアドレス空間が限られている場合は、上記の方法で仮想アドレス指定に依存します。スケーリングしません(たとえば、4GB RAM 32-bitアドレッシング)。一方、64-bitプロセスが32GBのRAMで実行されており、比較的このような文字列の割り当てが少ない場合は、膨大な量の仮想アドレス空間を操作でき、ページサイズへの切り上げはそれほど多くありません。
ただし、バッファ全体に書き込みを行ってから、バッファを以前の時点で終了する(この場合、一度書き込まれたメモリにはバッキングメモリがあり、最終的にはスワップになる可能性がある)と、書き込みを行うだけの大きなバッファを持つ場合の違いに注意してください。最初のビットに移動して終了します(この場合、バッキングメモリは、ページサイズに切り上げられた使用済みスペースにのみ割り当てられます)。
多くのオペレーティングシステムでは、プロセスが終了するまでヒープメモリがオペレーティングシステムに返されない可能性があることも指摘しておく必要があります。代わりに、malloc/freeライブラリは、ヒープを拡張する必要があるときにOSに通知します(たとえば、sbrk()
(UNIXの場合)またはVirtualAlloc()
(Windowsの場合))。その意味で、free()
メモリは、プロセスが再利用するために解放されますが、他のプロセスが使用するためには解放されません。一部のオペレーティングシステムは、これを最適化します。たとえば、非常に大きな割り当てに対して、個別に解放可能なメモリ領域を使用します。
Mallocを呼び出す前に必要なスペースを計算する前もってのプログラミング時間を節約するために、このぶら下がっているスペースをそこに残すことは、一般的に悪いプログラミングスタイルですか?
繰り返しますが、それはあなたが扱っているそのような割り当ての数に依存します。仮想アドレス空間に関連するものが非常に多い場合/ RAM-realloc()
を使用して、最初に要求されたすべてのメモリが実際に必要なわけではないことをメモリライブラリに明示的に通知します。または、strdup()
を使用して、実際のニーズに基づいて新しいブロックをより厳密に割り当てることもできます(その後、元のfree()
)-うまくいくか悪いかが決まるmalloc/freeライブラリの実装に応じて、ただし、違いの影響を大きく受けるアプリケーションはほとんどありません。
場合によっては、コードがライブラリにあり、呼び出し元のアプリケーションが管理する文字列インスタンスの数を推測できないことがあります。このような場合は、動作が遅くなりすぎて決して悪くならないようにする方がよいでしょう。元の文字列バッファーの未知の割合が無駄になるのではなく(任意の大きな割り当ての後に0または1文字が使用される)、文字列データ(追加の操作の設定数であるため、big-Oの効率に影響を与えません)に適合します。パフォーマンスの最適化として、使用されていない領域が使用済みの領域である場合にのみメモリを返す必要があるかもしれません-好みに合わせて調整するか、呼び出し側で構成可能にします。
あなたは別の答えにコメントします:
したがって、reallocに時間がかかるかどうか、または前処理のサイズ決定にかかる時間を判断することになりますか?
パフォーマンスが最優先事項である場合は、はい-プロファイルを作成します。 CPUバウンドでない場合は、一般的なルールとして、「前処理」ヒットを使用して適切なサイズの割り当てを実行します。断片化と混乱が少ないだけです。これに対抗するために、いくつかの関数のために特別な前処理モードを作成する必要がある場合、それはエラーとコードが維持する追加の「表面」です。 (このトレードオフの決定は、asprintf()
から独自のsnprintf()
を実装する場合に一般的に必要ですが、少なくともsnprintf()
は文書化されたとおりに機能し、個人的には持っていないことが信頼できますそれを維持するため)。
'\ 0'が追加されたら、メモリが返されるだけですか、それともfree()が呼び出されるまでメモリを占有し続けますか?
\0
に魔法のようなものはありません。割り当てられたメモリを「縮小」するには、realloc
を呼び出す必要があります。それ以外の場合は、free
を呼び出すまでメモリがそのまま残ります。
割り当てられたメモリの中央に「\ 0」を挿入しても、(a。)free()は引き続き正しく機能しますか
何をしてもそのメモリ内free
は、malloc
によって返されるまったく同じポインタを渡した場合、常に正しく動作します。もちろん、あなたがそれを外で書くならば、すべての賭けはオフです。
\0
はmalloc
とfree
の観点からもう1文字です。メモリにどのようなデータを置いてもかまいません。したがって、free
は、途中で\0
を追加しても、\0
をまったく追加しなくても機能します。割り当てられた余分なスペースはそのまま残り、\0
をメモリに追加してもすぐにはプロセスに返されません。個人的には、リソースを浪費するだけなので、上限に割り当てるのではなく、必要な量のメモリのみを割り当てることを好みます。
\0
は文字配列を文字列として解釈するための純粋な規則です。これはメモリ管理とは無関係です。つまり、お金を取り戻したい場合は、realloc
に電話する必要があります。文字列はメモリを気にしません(多くのセキュリティ問題の原因は何ですか)。
Malloc()を呼び出してヒープからメモリを取得するとすぐに、そのメモリを使用できます。\0の挿入は、他の文字の挿入に似ています。このメモリは、解放するか、OSが要求するまで、あなたの所有物に残ります。
mallocはメモリのチャンクを割り当てるだけです。使用するのはあなた次第ですが、最初のポインタの位置から自由に呼び出すことができます...途中に '\ 0'を挿入しても影響はありません...
具体的に言うと、mallocはどのタイプのメモリが必要かを認識していません(それはvoidポインタのみを返します)..
0x10から0x19までの10バイトのメモリを割り当てるとします。
char * ptr = (char *)malloc(sizeof(char) * 10);
5番目の位置(0x14)にnullを挿入しても、メモリ0x15以降は解放されません...
ただし、0x10から解放すると、10バイトのチャンク全体が解放されます。
free()
は引き続きメモリ内のNULバイトで動作します
スペースは、free()
が呼び出されるまで、または後で割り当てを縮小しない限り、無駄になります。
一般的に、メモリはメモリです。何を書き込んでもかまいません。しかし、それは人種を持っているか、またはフレーバー(malloc、new、VirtualAlloc、HeapAllocなど)を好む場合。つまり、メモリの一部を割り当てるパーティは、メモリの割り当てを解除する手段も提供する必要があります。 APIがDLLで提供されている場合は、何らかのフリー関数を提供する必要があります。もちろんこれは発信者に負担をかけますよね?では、呼び出し元に[〜#〜] whole [〜#〜]の負担をかけないのはなぜですか?動的に割り当てられたメモリを処理する最良の方法は[〜#〜] not [〜#〜]自分で割り当てることです。発信者に割り当てて渡してもらいます。彼は自分が割り当てたフレーバーを知っており、彼はそれを使い終わったらいつでもそれを解放する責任があります。
発信者はどのように割り当てるべき量を知っていますか?多くのWindows APIと同様に、関数は呼び出されたときに必要なバイト数を返します。 NULLポインターを使用して、NULL以外のポインターが提供されたときにジョブを実行します(ケースのアクセシビリティを再確認するのに適している場合は、IsBadWritePtrを使用します)。
これもはるかに効率的です。メモリの割り当ては多くのコストです。メモリ割り当てが多すぎると、ヒープの断片化が発生し、割り当てのコストがさらに高くなります。これが、カーネルモードでいわゆる「ルックアサイドリスト」を使用する理由です。行われるメモリ割り当ての数を最小限に抑えるために、NTカーネルがドライバーライターに提供するサービスを使用して、割り当てて「解放」したブロックを再利用します。呼び出し元にメモリ割り当ての責任を渡すと、呼び出し元に安価なメモリ(_alloca)が渡されるか、追加の割り当てなしで同じメモリが繰り返し渡される可能性があります。もちろん構いませんが、呼び出し元が最適なメモリ処理を担当することを許可します。
いくつかのMS Windows APIが行うことを、呼び出し元がポインターを渡し、割り当てたメモリのサイズで実行できます。サイズが十分でない場合は、割り当てるバイト数が表示されます。それが十分であった場合、メモリが使用され、結果は使用されたバイト数になります。
したがって、メモリを効率的に使用する方法についての決定は、呼び出し元に委ねられます。彼らは固定の255バイトを割り当て(Windowsでパスを操作する場合に一般的)、関数呼び出しの結果を使用して、より多くのバイトが必要かどうか(Win_32 APIをバイパスせずにMAX_PATHが255であるパスの場合は除く)、またはほとんど無視することができます...呼び出し側は、メモリサイズとしてゼロを渡し、割り当てる必要がある量を正確に通知することもできます-処理に関しては効率的ではありませんが、空間的には効率的です。
CでのNULLターミネーターの使用について詳しく説明するには、「C文字列」を割り当てることはできません。char配列を割り当ててその中に文字列を格納できますが、mallocとfreeは、要求された長さの配列としてそれを参照します。
C文字列はデータ型ではありませんが、ヌル文字 '\ 0'が文字列ターミネータとして扱われるchar配列を使用するための規則です。これは、長さの値を個別の引数として渡す必要なく、文字列を渡す方法です。他の一部のプログラミング言語には、文字データとともに長さを格納する明示的な文字列型があり、単一のパラメーターで文字列を渡すことができます。
引数を "C文字列"として文書化する関数には、char配列が渡されますが、nullターミネータがないと配列の大きさを知る方法がないため、存在しない場合はひどく間違ってしまいます。
必ずしも文字列として扱われるとは限らないchar配列を期待する関数では、常にバッファー長パラメーターを渡す必要があります。たとえば、ゼロバイトが有効な値であるcharデータを処理する場合、ターミネータ文字として「\ 0」を使用することはできません。
あなたは確かに上限に事前に割り当てることができ、すべてまたはそれ以下のものを使用できます。実際に使用するのがすべてかそれ以下であることを確認してください。
2パスを作成することもできます。
あなたはトレードオフについて正しい質問をしました。
どうやって決めるの?
次の理由により、最初は2つのパスを使用します。
1. you'll know you aren't wasting memory.
2. you're going to profile to find out where
you need to optimize for speed anyway.
3. upperbounds are hard to get right before
you've written and tested and modified and
used and updated the code in response to new
requirements for a while.
4. simplest thing that could possibly work.
コードも少しきつくなるかもしれません。通常は短いほど良いです。そして、コードが既知の真理を利用するほど、コードが言うことを実行することがより快適になります。
char* copyWithoutDuplicateChains(const char* str)
{
if (str == NULL) return NULL;
const char* s = str;
char prev = *s; // [prev][s+1]...
unsigned int outlen = 1; // first character counted
// Determine length necessary by mimicking processing
while (*s)
{ while (*++s == prev); // skip duplicates
++outlen; // new character encountered
prev = *s; // restart chain
}
// Construct output
char* outstr = (char*)malloc(outlen);
s = str;
*outstr++ = *s; // first character copied
while (*s)
{ while (*++s == prev); // skip duplicates
*outstr++ = *s; // copy new character
}
// done
return outstr;
}