96

Cでは、メモリの管理方法を実際に監視する必要があるといつも聞いています。そして、私はまだCを学び始めていますが、これまでのところ、関連するアクティビティを管理するメモリをまったく実行する必要はありませんでした。変数を解放し、あらゆる種類の醜いことをしなければならないことを常に想像していました。しかし、そうではないようです。

誰かが(コード例を使って)「メモリ管理」をしなければならないときの例を教えてもらえますか?

4

12 に答える 12

241

変数をメモリに入れることができる場所は2つあります。このような変数を作成する場合:

int  a;
char c;
char d[16];

変数は「スタック」に作成されます。スタック変数は、スコープ外になると(つまり、コードが到達できなくなったときに)自動的に解放されます。「自動」変数と呼ばれるものを聞くかもしれませんが、それは時代遅れになっています。

多くの初心者の例では、スタック変数のみを使用します。

スタックは自動であるため便利ですが、2つの欠点もあります。(1)コンパイラは変数の大きさを事前に知る必要があること、および(2)スタックスペースがいくらか制限されていることです。例:Windowsでは、Microsoftリンカのデフォルト設定では、スタックは1 MBに設定されており、すべてのスタックを変数で使用できるわけではありません。

コンパイル時に配列の大きさがわからない場合、または大きな配列や構造体が必要な場合は、「プランB」が必要です。

プランBは「ヒープ」と呼ばれます。通常、オペレーティングシステムで可能な大きさの変数を作成できますが、自分で作成する必要があります。以前の投稿では、他の方法もありますが、それを実行できる1つの方法が示されていました。

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(ヒープ内の変数は直接操作されるのではなく、ポインターを介して操作されることに注意してください)

ヒープ変数を作成すると、問題は、コンパイラーがそれがいつ終了したかを認識できないため、自動解放が失われることです。そこで、あなたが参照していた「手動解放」が登場します。コードは、変数が不要になる時期を決定し、他の目的でメモリを使用できるように解放する責任があります。上記の場合、次のようになります。

free(p);

この2番目のオプションを「厄介なビジネス」にしているのは、変数が不要になったときにそれを知るのは必ずしも簡単ではないということです。必要のないときに変数を解放するのを忘れると、プログラムは必要以上のメモリを消費します。この状況は「リーク」と呼ばれます。「リークされた」メモリは、プログラムが終了し、OSがすべてのリソースを回復するまで何にも使用できません。実際に使用する前に誤ってヒープ変数を解放すると、厄介な問題が発生する可能性があります。

CおよびC++では、上記のようにヒープ変数をクリーンアップする必要があります。ただし、JavaやC#などの.NET言語など、ヒープが独自にクリーンアップされる別のアプローチを使用する言語や環境があります。「ガベージコレクション」と呼ばれるこの2番目の方法は、開発者にとってははるかに簡単ですが、オーバーヘッドとパフォーマンスにペナルティがあります。それはバランスです。

(私はより単純ですが、うまくいけばより平準化された答えを与えるために多くの詳細をざっと見ました)

于 2008-08-24T08:21:22.927 に答える
19

これが例です。文字列を複製するstrdup()関数があるとします。

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

そして、あなたはそれをこのように呼びます:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

プログラムが機能していることがわかりますが、メモリを解放せずに(mallocを介して)割り当てています。2回目にstrdupを呼び出したときに、最初のメモリブロックへのポインタを失いました。

これは、この少量のメモリにとっては大したことではありませんが、次の場合を考えてみてください。

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

これで11ギガのメモリを使い果たしました(メモリマネージャによってはそれ以上になる可能性があります)。クラッシュしていない場合は、プロセスの実行速度がかなり遅い可能性があります。

修正するには、malloc()の使用を終了した後、malloc()で取得されるすべてのものに対してfree()を呼び出す必要があります。

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

この例がお役に立てば幸いです。

于 2008-08-24T07:38:05.137 に答える
9

スタックではなくヒープでメモリを使用する場合は、「メモリ管理」を実行する必要があります。実行時まで配列を作成するサイズがわからない場合は、ヒープを使用する必要があります。たとえば、文字列に何かを格納したいが、プログラムが実行されるまでその内容の大きさがわからない場合があります。その場合、次のように記述します。

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
于 2008-08-24T06:57:26.090 に答える
6

質問に答える最も簡潔な方法は、C でのポインターの役割を検討することだと思います。ポインターは、軽量でありながら強力なメカニズムであり、自分自身を撃つ可能性が非常に高いという代償を払って、非常に自由を与えてくれます。

C では、ポインタが自分が所有するメモリを指すようにする責任は、自分だけのものです。これには、効率的な C の記述を困難にするポインターを放棄しない限り、組織的で規律あるアプローチが必要です。

これまでに投稿された回答は、自動 (スタック) およびヒープ変数の割り当てに集中しています。スタック割り当てを使用すると、自動的に管理された便利なメモリが作成されますが、状況によっては (大きなバッファ、再帰アルゴリズム)、スタック オーバーフローという恐ろしい問題が発生する可能性があります。スタックに割り当てることができるメモリの量を正確に知ることは、システムに大きく依存します。一部の組み込みシナリオでは数十バイトが限界かもしれませんが、一部のデスクトップ シナリオではメガバイトを安全に使用できます。

ヒープ割り当ては、言語固有のものではありません。これは基本的に、指定されたサイズのメモリ ブロックを返す (「解放する」) 準備ができるまで、そのブロックの所有権を付与する一連のライブラリ呼び出しです。それは単純に聞こえますが、計り知れないほどのプログラマーの悲しみに関連しています。問題は単純 (同じメモリを 2 回解放する、またはまったく解放しない [メモリ リーク]、十分なメモリを割り当てない [バッファ オーバーフロー] など) ですが、回避してデバッグするのは困難です。高度に訓練されたアプローチは実際には絶対に必須ですが、もちろん、言語が実際にそれを義務付けているわけではありません。

他の投稿では無視されている別の種類のメモリ割り当てについて言及したいと思います。関数の外で変数を宣言することにより、変数を静的に割り当てることができます。一般に、このタイプの割り当ては、グローバル変数によって使用されるため、評判が悪いと思います。ただし、この方法で割り当てられたメモリを使用する唯一の方法は、乱雑なスパゲッティ コード内の規律のないグローバル変数として使用することであると言うものは何もありません。静的割り当て方法は、ヒープおよび自動割り当て方法のいくつかの落とし穴を回避するために単純に使用できます。一部の C プログラマーは、大規模で洗練された C 組み込みプログラムやゲーム プログラムが、ヒープ割り当てをまったく使用せずに構築されていることを知って驚いています。

于 2008-09-02T03:39:29.537 に答える
4

ここには、メモリの割り当てと解放の方法についていくつかの優れた回答があります。私の意見では、Cを使用することのより難しい側面は、使用するメモリが割り当てたメモリだけであることを確認することです。このサイトのいとこ(バッファオーバーフロー)があり、別のアプリケーションで使用されているメモリを上書きしている可能性があり、非常に予測できない結果になります。

例:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

この時点で、myStringに5バイトを割り当て、「abcd \ 0」で埋めました(文字列はnullで終わります-\ 0)。文字列の割り当てが

myString = "abcde";

プログラムに割り当てた5バイトに「abcde」を割り当て、末尾のヌル文字をこの最後に配置します。これは、使用に割り当てられていないメモリの一部であり、次のようになります。無料ですが、別のアプリケーションでも同様に使用される可能性があります-これはメモリ管理の重要な部分であり、ミスが予測できない(場合によっては再現できない)結果をもたらす可能性があります。

于 2008-08-24T07:58:13.387 に答える
4

初期化されていないポインタには疑似乱数の有効なメモリアドレスが含まれている可能性があり、ポインタエラーが静かに進行する可能性があるためです。ポインターを NULL で初期化することを強制することにより、このポインターを初期化せずに使用している場合は常にキャッチできます。その理由は、オペレーティング システムが仮想アドレス 0x00000000 を一般保護例外に「配線」して、null ポインターの使用をトラップするためです。

于 2009-03-29T23:00:00.727 に答える
2

また、int [10000]などの巨大な配列を定義する必要がある場合は、動的メモリ割り当てを使用することもできます。スタックに入れることはできません。そうすると、スタックオーバーフローが発生するからです。

もう1つの良い例は、リンクリストやバイナリツリーなどのデータ構造の実装です。ここに貼り付けるサンプルコードはありませんが、簡単にグーグルで検索できます。

于 2008-08-24T07:50:49.473 に答える
2

(これまでの答えが的外れだと感じたので書いています。)

言及する価値のあるメモリ管理が必要な理由は、複雑な構造を作成する必要がある問題/解決策がある場合です。(一度に大量のスペースをスタックに割り当てるとプログラムがクラッシュする場合、それはバグです。) 通常、最初に学ぶ必要があるデータ構造は、ある種のlistです。これは、私の頭の上から離れた、単一のリンクされたものです。

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

当然、他のいくつかの関数が必要になりますが、基本的には、これがメモリ管理に必要なものです。「手動」メモリ管理で可能ないくつかのトリックがあることを指摘しておく必要があります。

  • mallocが (言語標準によって) 4 で割り切れるポインターを返すことが保証されているという事実を利用して、
  • あなた自身の不吉な目的のために余分なスペースを割り当て、
  • メモリプールを作成しています..

良いデバッガーを入手してください...頑張ってください!

于 2008-08-24T14:13:13.063 に答える
0

@ Ted Percival
... malloc()の戻り値をキャストする必要はありません。

もちろん、あなたは正しいです。チェックするK&Rのコピーはありませんが、それは常に真実だと思います。

私はCでの暗黙の変換の多くが好きではないので、「魔法」をより見やすくするためにキャストを使用する傾向があります。読みやすさを向上させる場合もあれば、そうでない場合もあります。また、コンパイラがサイレントバグを検出する原因となる場合もあります。それでも、私はこれについて、何らかの形で強い意見を持っていません。

これは、コンパイラがC++スタイルのコメントを理解している場合に特に発生しやすくなります。

ええ...あなたはそこで私を捕まえました。私はCよりもC++で多くの時間を費やしています。それに気づいてくれてありがとう。

于 2008-08-25T16:09:04.477 に答える
0

@ユーロ・ミチェリ

追加すべき欠点の 1 つは、スタックへのポインターが関数から戻るときに無効になるため、関数からスタック変数へのポインターを返すことができないことです。これは一般的なエラーであり、スタック変数だけではうまくいかない主な理由です。関数がポインターを返す必要がある場合は、malloc してメモリ管理を処理する必要があります。

于 2008-08-25T16:50:17.780 に答える
0

C では、実際には 2 つの異なる選択肢があります。1 つ目は、システムにメモリを管理させることができます。または、自分でそれを行うこともできます。一般に、できる限り前者に固執することをお勧めします。ただし、C の自動管理メモリは非常に限られているため、次のような多くの場合、メモリを手動で管理する必要があります。

を。変数を関数よりも長く存続させたいが、グローバル変数を持ちたくない。元:

構造体ペア{
   int 値;
   構造体のペア * 次;
}

構造体ペア* new_pair(int val){
   構造体のペア* np = malloc(sizeof(構造体のペア));
   np->val = val;
   np->next = NULL;
   np を返します。
}

b. 動的に割り当てられたメモリが必要です。最も一般的な例は、固定長のない配列です。

int *my_special_array;
my_special_array = malloc(sizeof(int) * number_of_element);
for(i=0; i

c. あなたは本当に汚いことをしたいのです。たとえば、構造体で多くの種類のデータを表現したいのですが、ユニオンは好きではありません (ユニオンはめちゃくちゃに見えます)。

構造体データ{ int data_type; 長い data_in_mem; }; struct animal{/*something*/}; struct person{/*some other thing*/}; struct animal* read_animal(); struct person* read_person(); /*メインで*/ 構造体データ サンプル。 sample.data_type = input_type; スイッチ(入力タイプ){ ケースDATA_PERSON: sample.data_in_mem = read_person(); 壊す; ケースDATA_ANIMAL: sample.data_in_mem = read_animal(); デフォルト: printf("ああ、もう一度警告します。あなたの OS をセグ フォールトさせます"); }

ほら、長い値は何でも保持するのに十分です。それを解放することを忘れないでください。さもないと後悔します。これは、C :D を楽しむための私のお気に入りのトリックの 1 つです。

ただし、一般的には、お気に入りのトリックには近づかないでください (T___T)。OS を頻繁に使用すると、遅かれ早かれ OS が壊れます。*alloc と free を使用しない限り、あなたは未使用のままであり、コードはまだ見栄えが良いと言って差し支えありません。

于 2008-08-27T12:08:54.443 に答える
-3

もちろん。使用するスコープの外に存在するオブジェクトを作成する場合。これは不自然な例です(構文がオフになることに注意してください。Cは錆びていますが、この例でも概念を説明します)。

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

この例では、MyClassの存続期間中にSomeOtherClassタイプのオブジェクトを使用しています。SomeOtherClassオブジェクトはいくつかの関数で使用されるため、メモリを動的に割り当てました。SomeOtherClassオブジェクトは、MyClassの作成時に作成され、オブジェクトの存続期間中に数回使用され、MyClassが解放されると解放されます。

明らかに、これが実際のコードである場合、この方法でmyObjectを作成する理由は(スタックメモリの消費を除いて)ありませんが、このタイプのオブジェクトの作成/破棄は、オブジェクトが多く、細かく制御したい場合に役立ちます。それらが作成および破棄されたとき(たとえば、アプリケーションがその存続期間全体で1GBのRAMを消費しないようにするため)、ウィンドウ環境では、作成するオブジェクト(ボタンなど)として、これはほぼ必須です。 、特定の関数(またはクラス)のスコープ外に存在する必要があります。

于 2008-08-24T07:00:35.320 に答える