ローカル変数のメモリはスコープ外からアクセスできますか? 質問する

ローカル変数のメモリはスコープ外からアクセスできますか? 質問する

次のコードがあります。

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

そして、コードは実行時例外なしで実行されます。

出力は58

どうしてそうなるのでしょうか? ローカル変数のメモリは関数外からはアクセスできないのではないでしょうか?

ベストアンサー1

どうしてそうなるのでしょうか? ローカル変数のメモリは関数外からはアクセスできないのではないでしょうか?

ホテルの部屋を借ります。ベッドサイドテーブルの一番上の引き出しに本を入れて眠ります。翌朝チェックアウトしますが、鍵を返すのを「忘れて」しまいます。鍵を盗んでしまうのです。

1 週間後、ホテルに戻り、チェックインせずに盗んだ鍵で以前の部屋に忍び込み、引き出しの中を覗いてみましょう。本はまだそこにあります。驚きです!

どうしてそうなるのでしょう?ホテルの部屋の引き出しの中身は、部屋を借りていないとアクセスできないのではないですか?

まあ、明らかに、そのシナリオは現実世界で問題なく起こり得ます。部屋に入る権限がなくなったときに本が消えてしまうような不思議な力は存在しません。また、盗まれた鍵で部屋に入ることを妨げるような不思議な力も存在しません。

ホテルの管理者はあなたの本を撤去する必要はありません。あなたはホテルと、あなたが何かを置き忘れたらシュレッダーにかけてくれるという契約を結んでいません。盗んだ鍵を使って部屋を違法に再入場して鍵を取り戻そうとした場合、ホテルの警備員はあなたが忍び込んだのを捕まえる必要はありません。あなたはホテルと、「私が後で部屋に忍び込もうとしたら、あなたは私を阻止する必要があります」という契約を結んでいません。むしろ、あなたはホテルと、「後で部屋に忍び込まないことを約束します」という契約を結び、その契約を破ったのです

この状況では、何が起きてもおかしくありません。本がそこにあるかもしれません。あなたは運がよかっただけかもしれません。誰か他の人の本がそこにあり、あなたの本はホテルの炉の中にあるかもしれません。あなたが入った瞬間に誰かがそこにいて、あなたの本をずたずたに引き裂いているかもしれません。ホテルがテーブルと本を完全に取り除き、代わりにワードローブを置いているかもしれません。ホテル全体が今にも取り壊されてフットボール スタジアムに置き換えられ、あなたがこっそりと歩き回っている間に爆発で死ぬことになるかもしれません。

何が起こるかは分かりません。ホテルをチェックアウトして鍵を盗み、後で違法に使用した場合、システムのルールを破ることを選択したため、予測可能で安全な世界で生きる権利を放棄したことになります

C++ は安全な言語ではありません。システムのルールを破ることを平気で許します。許可されていない部屋に戻って、もうそこにないかもしれない机の中を漁るなど、違法で愚かなことをしようとしても、C++ はそれを阻止できません。C++ よりも安全な言語は、キーをより厳密に制御するなど、権限を制限することでこの問題を解決します。


コンパイラは、プログラムによって操作されるデータのストレージを管理するコードを生成します。メモリを管理するコードを生成する方法は多種多様ですが、時間の経過とともに 2 つの基本的な手法が定着してきました。

1 つ目は、ストレージ内の各バイトの「存続期間」、つまり、それが何らかのプログラム変数に有効に関連付けられている期間を事前に簡単に予測できない、ある種の「長期存続」ストレージ領域を持つことです。コンパイラは、必要なときにストレージを動的に割り当て、不要になったときにそれを再利用する方法を知っている「ヒープ マネージャ」への呼び出しを生成します。

2 番目の方法は、各バイトの有効期間がわかっている「短命」のストレージ領域を用意することです。この場合、有効期間は「ネスト」パターンに従います。これらの短命変数のうち最も長い有効期間を持つ変数は、他の短命変数の前に割り当てられ、最後に解放されます。より短い有効期間を持つ変数は、最も長い有効期間を持つ変数の後に割り当てられ、より長い有効期間を持つ変数の前に解放されます。これらの短命変数の有効期間は、より長い有効期間を持つ変数の有効期間内に「ネスト」されます。

ローカル変数は後者のパターンに従います。つまり、メソッドに入ると、そのローカル変数が有効になります。そのメソッドが別のメソッドを呼び出すと、新しいメソッドのローカル変数が有効になります。最初のメソッドのローカル変数が無効になる前に、新しいメソッドのローカル変数は無効になります。ローカル変数に関連付けられたストレージの有効期間の開始と終了の相対的な順序は、事前に決定できます。

このため、ローカル変数は通常、「スタック」データ構造上のストレージとして生成されます。これは、スタックには、最初にプッシュされたものが最後にポップオフされるという特性があるためです。

ホテル側が部屋を順番に貸し出すことに決めていて、自分より部屋番号が大きい人全員がチェックアウトするまでチェックアウトできないような感じです。

では、スタックについて考えてみましょう。多くのオペレーティング システムでは、スレッドごとに 1 つのスタックが用意され、スタックは特定の固定サイズに割り当てられます。メソッドを呼び出すと、スタックにデータがプッシュされます。元の投稿者がここで行っているように、メソッドからスタックへのポインターを渡すと、それは完全に有効な 100 万バイトのメモリ ブロックの中央へのポインターになります。この例えで言うと、ホテルをチェックアウトするとします。チェックアウトすると、最も高い番号の部屋からチェックアウトしたことになります。その後に誰もチェックインせず、違法に部屋に戻った場合、すべてのアイテムはこのホテルにまだ残っていることが保証されます。

スタックは、非常に安価で簡単なので、一時的な保存に使用します。C++ の実装では、ローカルの保存にスタックを使用する必要はありません。ヒープを使用できます。プログラムが遅くなるため、スタックは使用しません。

C++ の実装では、スタック上に残したゴミをそのままにして、後で不正に取り戻せるようにする必要はありません。コンパイラが、空けた「部屋」内のすべてのものをゼロに戻すコードを生成することは完全に合法です。繰り返しますが、コストがかかるので、そうしません。

C++ の実装では、スタックが論理的に縮小したときに、有効だったアドレスがメモリにマップされたままであることを保証する必要はありません。実装では、オペレーティング システムに「スタックのこのページの使用はこれで終了です。他に指示がない限り、以前に有効だったスタック ページに誰かが触れた場合は、プロセスを破壊する例外を発行してください」と伝えることができます。繰り返しますが、実装では実際にはこれを行いません。これは、時間がかかり、不必要だからです。

代わりに、実装によってミスを犯しても、ほとんどの場合は問題なく済みます。しかし、ある日、本当にひどい問題が発生し、プロセスが破綻します。

これは問題です。ルールはたくさんあり、誤って破ってしまうことは非常に簡単です。私も確かに何度も破ってしまいました。さらに悪いことに、メモリ破損が起こってから数十億ナノ秒後にメモリ破損が検出されたときに初めて問題が表面化することが多く、その場合誰がそれを台無しにしたのかを突き止めるのは非常に困難です。

メモリセーフな言語は、権限を制限することでこの問題を解決します。「通常の」C# では、ローカルのアドレスを取得して返したり、後で使用できるように保存したりする方法はありません。ローカルのアドレスを取得することはできますが、言語は巧妙に設計されているため、ローカルの有効期間が終了した後は使用できません。ローカルのアドレスを取得して返すには、コンパイラを特別な「安全でない」モードに設定し、プログラムに「安全でない」という単語を入れて、ルールに違反する可能性のある危険な操作を行っている可能性があることに注意を喚起する必要があります。

さらに詳しく読むには:

  • C# で参照を返すことができたらどうなるでしょうか? 偶然にも、それが今日のブログ投稿の主題です。

    ref 戻り値と ref ローカル

  • メモリを管理するためにスタックを使用するのはなぜですか? C# の値型は常にスタックに格納されますか? 仮想メモリはどのように機能しますか? C# メモリ マネージャーの仕組みに関するトピックは他にもたくさんあります。これらの記事の多くは、C++ プログラマーにも関連しています。

    メモリ管理

おすすめ記事