Cメモリ管理


90

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

誰かが「コード管理」を使って、「メモリ管理」をしなければならないときの例を見せてくれますか?


回答:


230

変数をメモリに配置できる場所は2つあります。次のような変数を作成すると、

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

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

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

スタックは自動なので便利ですが、次の2つの欠点もあります。(1)コンパイラーは変数の大きさを事前に知っておく必要がある、(b)スタックスペースがいくらか制限されている。例: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番目の方法は、開発者にとってははるかに簡単ですが、オーバーヘッドとパフォーマンスの点で不利になります。それはバランスです。

(私は多くの詳細を省略して、より単純ですがうまくいけばより平準化された答えを提供します)


3
スタックに何かを置きたいが、それがコンパイル時にどれほど大きいかわからない場合、alloca()はスタックフレームを拡大してスペースを空けることができます。freea()はありません。関数が戻ると、スタックフレーム全体がポップされます。大きな割り当てにalloca()を使用すると、危険が伴います。
DGentry 2008

1
たぶん、グローバル変数のメモリ位置に関する1つまたは2つの文を追加できます
MichaelKäferJan

C決してキャストリターンではmalloc()、その原因のUB、(char *)malloc(size);参照stackoverflow.com/questions/605845/...
EsmaeelE

17

ここに例があります。文字列を複製する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()を使用して取得したものすべてを使用後にfree()を呼び出す必要があります。

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

この例が役立つことを願っています!


私はこの答えが好きです。しかし、少し補足的な質問がありました。このようなものがライブラリで解決されることを期待していますが、基本的なデータ型を厳密に模倣し、メモリ解放機能を追加して、変数が使用されると自動的に解放されるライブラリはありませんか?
ロレンソ

標準の一部ではありません。C ++に入ると、自動メモリ管理を行う文字列とコンテナが得られます。
マークハリソン

なるほど、サードパーティのライブラリはいくつかありますか?それらに名前を付けてもらえますか?
ロレンソ

9

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

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

 // Do something with the string...

 free(string); // Free the allocated memory

5

Cでのポインターの役割を検討するには、この質問に答える最も簡潔な方法だと思います。ポインターは軽量でありながら強力なメカニズムであり、足で自分を撃つための莫大な能力を犠牲にして非常に自由です。

Cでは、ポインタがあなたが所有するメモリを指すようにする責任は、あなたとあなただけです。ポインタを捨てない限り、これには組織化された統制のとれたアプローチが必要です。これにより、効果的なCを書くことが難しくなります。

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

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

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


4

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

例:

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

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

myString = "abcde";

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


ここでは5バイトを割り当てます。ポインタを割り当てて緩めます。このポインタを解放しようとすると、未定義の動作が発生します。C-Stringは=演算子をオーバーロードしないことに注意してください。コピーはありません。
マーティンヨーク

ただし、実際に使用するmallocによって異なります。多くのmallocオペレーターは8バイトに調整されます。したがって、このmallocがヘッダー/フッターシステムを使用している場合、mallocは5 + 4 * 2(ヘッダーとフッターの両方に4バイト)を予約します。それは13バイトであり、mallocはアライメントのためにさらに3バイトを提供します。これは、mallocがこのように機能するシステムのみであるため、これを使用することは良い考えだとは言っていませんが、少なくとも何かがおかしいとうまくいくかもしれない理由を知ることは少なくとも重要です。
kodai 2009

ロキ:のstrcpy()代わりに使用するように回答を編集しました=。それはクリス・BCの意図だったと思います。
echristopherson 2013

最近のプラットフォームでは、ハードウェアのメモリ保護により、ユーザースペースプロセスが他のプロセスのアドレススペースを上書きすることを防いでいると思います。代わりにセグメンテーション違反が発生します。しかし、それ自体はCの一部ではありません。
echristopherson 2013

4

覚えておくべきことは、ポインタを常に NULLに初期化することです。これは、初期化されていないポインタには、ランダムに有効なメモリアドレスが含まれ、ポインタエラーが発生する可能性があるためです。ポインターを強制的にNULLで初期化することにより、初期化せずにこのポインターを使用しているかどうかを常にキャッチできます。その理由は、オペレーティングシステムが仮想アドレス0x00000000を一般保護例外に「配線」して、ヌルポインターの使用をトラップするためです。


2

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

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


2

(私が書いているのは、これまでのところ答えが定かではないと感じているからです。)

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

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で割り切れるポインターを返す
  • あなた自身の不吉な目的のために余分なスペースを割り当てる、
  • メモリプールを作成しています ...

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


データ構造の学習は、メモリ管理を理解するための次の重要なステップです。これらの構造を適切に実行するためのアルゴリズムを学習すると、これらの問題を克服するための適切な方法がわかります。これが、同じコースで教えられているデータ構造とアルゴリズムを見つける理由です。
aj.toulan 2013年

0

@ユーロミセリ

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


0

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

もちろん、あなたは正しいです。私はK&Rのコピーを持っていませんが、それは常に真実であると信じています確認する。

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

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

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


@echristopherson、ありがとう。そのとおりです。ただし、このQ / Aは、スタックオーバーフローが公開ベータ版になる前の2008年8月のものであることに注意してください。その当時、私たちはまだサイトがどのように機能するかを考えていました。この質問/回答の形式は、必ずしもSOの使用方法のモデルと見なすべきではありません。ありがとう!
Euro Micelli 2013

ああ、指摘してくれてありがとう-その当時、サイトの側面がまだ流動的であるとは知りませんでした。
echristopherson 2013

0

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

a。変数が関数よりも長く存続することを望み、グローバル変数を持つことは望みません。例:

構造体ペア{
   int val;
   構造体ペア*次;
}

struct pair * new_pair(int val){
   struct pair * np = malloc(sizeof(struct pair));
   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。あなたは本当に汚いことをしたいです。たとえば、私は構造体が多くの種類のデータを表すようにしたいのですが、unionは好きではありません(unionは非常に乱雑に見えます)。

構造体データ{ int data_type; long data_in_mem; }; struct animal {/ * something * /}; struct person {/ *何か他のもの* /}; struct animal * read_animal(); struct person * read_person(); / *メイン* / 構造体データサンプル; sampe.data_type = input_type; スイッチ(入力タイプ){ ケースDATA_PERSON: sample.data_in_mem = read_person(); ブレーク; ケースDATA_ANIMAL: sample.data_in_mem = read_animal(); デフォルト: printf( "Oh hoh!I warn you、that again and I I seg fault your OS"); }

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

ただし、一般的には、お気に入りのトリック(T___T)は避けたいものです。頻繁に使用すると、遅かれ早かれOSが壊れてしまいます。* allocとfreeを使用しない限り、あなたはまだ処女であり、コードは美しく見えると言っても安全です。


「参照してください、長い値は何でも保持するのに十分です」-:/ほとんどのシステムでは、長い値は4バイトで、intとまったく同じです。ここでポインタに適合する唯一の理由は、longのサイズがたまたまポインタのサイズと同じになるためです。ただし、実際にはvoid *を使用する必要があります。
Score_Under

-2

承知しました。使用するスコープの外に存在するオブジェクトを作成する場合。これは不自然な例です(私の構文は無効になることに注意してください。私の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を消費しないようにするため)、およびウィンドウ環境では、これは、作成するオブジェクト(ボタンなど)としてかなり必須です。 、特定の関数(またはクラス)のスコープの外側に存在する必要があります。


1
ええ、そう、それはC ++ですね。誰かが私に電話をかけるのに5か月かかったのは驚くべきことです。
TheSmurf 2009年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.