Menu

C言語のスタックとヒープ:生存期間、ダングリングポインタ、使い分け

データが実際にどこに置かれるか:スタック上の自動記憶域、ヒープ上の動的記憶域、そして第3の領域である静的記憶域 - ローカル変数を返すことで生じる古典的なダングリングポインタのバグと、選び方の指針とともに。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Cプログラムのどの変数もどこかに置かれ、どこに置かれるかが、あとから変えられない2つのことを決めます。どれだけ長く生き残るか、そしてどれだけの量を持てるかです。Cは3つの記憶領域を用意しており、選び違えるとクラッシュかリークのどちらかが起きます。このページはそれらを整理し、生存期間を取り違えたときの古典的なバグを示します。

3つの領域

  高位アドレス
  +---------------------------+
  |  スタック                 |  ローカル変数、引数、戻り先アドレス
  |    下へ伸びる      |      |  戻るときに自動で解放される
  |                    v      |
  +---------------------------+
  |         (未使用の隙間)  |
  +---------------------------+
  |                    ^      |
  |    上へ伸びる      |      |
  |  ヒープ                   |  malloc / calloc / realloc のブロック
  +---------------------------+  free() でのみ解放される
  |  静的/グローバルデータ   |  グローバル変数と static、実行の全期間
  +---------------------------+
  |  コード (text)            |  機械語、読み取り専用
  +---------------------------+
  低位アドレス
  • 自動記憶域(スタック) は関数の引数と static でないローカル変数を保持します。関数に入るときにスタックの一区画が確保され、戻るときに解放されます。サイズはコンパイル時に固定されます。
  • 動的記憶域(ヒープ)malloccallocrealloc から得られるものすべてを保持します。サイズは実行時に決まり、生存期間は free でのみ終わります。
  • 静的記憶域 はグローバル変数と static と宣言されたものすべてを保持します。プログラムの実行全体を通じて存在し、main が始まる前にゼロ初期化されます。

図のアドレス配置は通常の並び方であって、保証ではありません - 標準が記述しているのは生存期間であって、配置ではないのです。

自動記憶域の実際

demo の呼び出しごとに新しい local と新しい table が得られます。手作業で解放するものは何もなく、リークし得ず、確保にかかるのはスタックポインタを動かす命令1つです。だからふつうのローカル変数を既定にすべきなのです。Cが持つ最も速く、最も安全な記憶領域だからです。

落とし穴は閉じ波かっこです。それが実行された瞬間、そのメモリは消えます。

ダングリングポインタ

Cプログラマなら誰でも一度は書くバグです。

/* 壊れている: もう存在しないメモリのアドレスを返している */
int *make_counter(void) {
    int count = 0;
    return &count;          /* count はこの波かっこで死ぬ */
}

int main(void) {
    int *p = make_counter();
    *p = 5;                 /* 死んだスタックフレームへの書き込み */
    return 0;
}

&countmake_counter が走っている間は完全に有効なアドレスでした。戻った時点で、そのスタック領域は次に呼ばれる関数に引き渡されるので、p は今や誰か他人のローカル変数を指しています。読めばゴミが、書けばそれらが壊れます。GCCとClangはまさにこの形について警告する(-Wreturn-local-addr)ので、警告を有効にしてコンパイルしましょう。

同じバグは配列では姿を変え、そこでは警告が出ないことも多いのです。

この関数の壊れた版は、ローカルな char buf[64] の中にテキストを組み立てて return buf; するでしょう - 同じ瞬間に存在しなくなるバッファのアドレスを返すのです。

3つの直し方

1. 呼び出し元がバッファを用意する(上に示したもの)。確保もなく、所有権の問題もなく、Cのライブラリで最もよくある流儀です。関数はその内側に収まるようサイズを受け取ります。

2. ヒープのメモリを返し、誰が解放するかを明記する。

ヒープのブロックは設計上、関数より長生きします - それが動的メモリの眼目です。代償は所有権を述べるコメントと、呼び出し元の free です。

3. 静的記憶域を使う。共有バッファが1つで構わないときです。

関数の中の static は変数のスコープをローカルに保ったままプログラムの生存期間を与えるので、そのアドレスを返すのは合法です。代償は、それが常にただ1つしかないことです。すべての呼び出し元がそれを共有するので、このパターンはスレッドを使うコードでは使えませんし、シングルスレッドでも2つの呼び出し元が同時にそのポインタを保持すれば驚く結果になります。

サイズ:スタックが尽きる場所

スタックの領域は小さく、固定されています。メインスレッドはWindowsで通常 1 MB、Linuxで 8 MB を得ます。生成されたスレッドはずっと少ないことがよくあります。ヒープはシステムの利用可能なメモリで区切られます。

void bad(void) {
    int huge[1000000];      /* 約 4 MB のスタック - 入った時点でおそらくクラッシュ */
    huge[0] = 1;
}

診断もなく、確認できる NULL もありません。プログラムは単に死にます。たいていはセグメンテーション違反で、本体の最初の行が走る前にです。ヒープ版はきちんと失敗を報告します。

深い再帰も同じようにフレーム1つずつスタックを使い切ります - 実務ではスタックオーバーフローの最もよくある原因が、暴走した再帰関数です。

コストと局所性

スタックの確保はレジスタに対する1回の算術演算です。ヒープの確保は適切なブロックを探すライブラリ呼び出しで、ロックを取るかもしれませんし、ときにはOSにもっとメモリを要求します。よく実行されるループの中では、この差は測定できます。

スタックのデータはまた密に詰まっていて最近触れられているので、キャッシュに載りやすい傾向があります。ヒープのブロックは散らばりうります。どちらの事実も単独で設計を左右すべきではありません - 生存期間の正しさが先です - が、どちらも正しい2つの設計の間では、たいていスタックのほうが速いほうです。

領域を見てみる

アドレスを表示すると配置が具体的になります。正確な値は実行ごとに変わります(現代のシステムはランダム化します)が、まとまりは見て取れます。

グローバル変数と静的変数は隣り合って並び、ヒープのブロックは別の場所にあり、ローカル変数はたいていその両方から遠くにあります。%p には void * へキャストしてください - 書式指定子が要求するものです。

選び方

スタックを使うのは次のときです。

  • サイズがコンパイル時に分かっている、
  • データがこの関数とそれが呼ぶ関数の中でしか必要ない、
  • そして小さい - メガバイトではなく数キロバイト。

ヒープを使うのは次のときです。

  • サイズが入力、ファイル、あるいは計算に依存する、
  • データがそれを作った関数より長生きしなければならない、
  • あるいはスタックの上限を脅かすほど大きい。

静的記憶域を使うのは次のときです。

  • プログラム全体を通じてちょうど1つだけ実体が存在すべきで、
  • すべての呼び出し元でそれを共有することが本当に正しいとき。

既定はスタックです。3つの理由のどれかが当てはまるときにヒープへ手を伸ばし、そうするときはメモリリークの所有権の規則に従って、生存期間を得たそのブロックがちゃんと解放されるようにしましょう。

鏡像のような2つのバグ

この2つは一緒に名指しする価値があります。同じ生存期間の問いに2通りの答えを出したものだからです。

  • ダングリングポインタ - ポインタより先にメモリが死んだ場合。&local を返す、あるいは free のあとでポインタを使う。プログラムは今や別のものに属する記憶領域を読み書きします。
  • メモリリーク - メモリより先にポインタが死んだ場合。malloc したブロックへの最後の参照を失う。すぐには何も壊れず、プロセスがただ肥大していきます。

どちらもデータがどれだけ長く生きなければならないかと、それをどの領域に置いたかの不一致から生まれます。まず生存期間を決めれば、領域はそれについてきます。

よくある質問

C言語のスタックとヒープの違いは何ですか?

スタックはローカル変数を保持します。サイズはコンパイラが決め、関数に入るときに作られ戻るときに破棄され、確保のコストはゼロです。ヒープは malloc のブロックを保持します。サイズは実行時に選び、ブロックは free するまで生き残り、確保には実際のコストがかかります。

なぜC言語でローカル変数へのポインタを返せないのですか?

関数が戻った瞬間にそのローカル変数の記憶領域が解放されるからです。ポインタはまだそのアドレスを持っていますが、そのメモリはもう次の関数呼び出しのものです - 読めばゴミが得られ、書けば無関係なデータを壊します。それがダングリングポインタです。malloc したブロックを返すか、呼び出し元にバッファを用意させましょう。

C言語のスタックはどれくらいの大きさですか?

メインスレッドで通常 1〜8 MB、追加のスレッドではもっと小さくなります。int big[1000000]; をローカル変数として置けば関数に入った時点でたいていクラッシュする程度の大きさです。ヒープは利用可能なシステムメモリによって制限されるので、大きいデータやサイズ不明のデータはそちらに置きます。

C言語でスタックではなくヒープを使うべきなのはいつですか?

3つの場合です。サイズが実行時まで分からないとき、データがそれを作った関数より長生きしなければならないとき、あるいはブロックがスタックに対して大きすぎるとき(おおよそ数百キロバイトを超えるもの)。それ以外はすべて素のローカル変数にすべきです - 速く、そしてリークし得ません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める