1125

私は次のコードを持っています。

#include <iostream>

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

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

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

出力は58

どうしてそれができますか?ローカル変数のメモリは、その関数の外ではアクセスできませんか?

4

21 に答える 21

4931

どうしてそれができますか?ローカル変数のメモリは、その関数の外ではアクセスできませんか?

あなたはホテルの部屋を借ります。ベッドサイドテーブルの一番上の引き出しに本を置いて寝ます。あなたは翌朝チェックアウトしますが、あなたの鍵を返すことを「忘れて」ください。あなたは鍵を盗みます!

1週間後、ホテルに戻り、チェックインせず、盗まれた鍵を持って古い部屋に忍び込み、引き出しを調べます。あなたの本はまだそこにあります。驚くべき!

どうしてそれができるのでしょうか?部屋を借りていない場合、ホテルの部屋の引き出しの中身にアクセスできませんか?

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

ホテルの管理者はあなたの本を削除する必要はありません。あなたは彼らと契約を結んでおらず、あなたが物を置き去りにすると、彼らはあなたのためにそれを細断するだろうと言っていました。盗まれた鍵を持って不法に部屋に戻って部屋に戻った場合、ホテルのセキュリティスタッフはあなたが忍び込んだのを捕まえる必要はありません。あなたは彼らと契約を結んでいませんでした。部屋の後で、あなたは私を止める必要があります。」むしろ、あなたは彼らと「後で私の部屋に忍び込まないことを約束します」という契約に署名しました。それはあなたが破った契約です。

この状況では、何でも起こり得ます。本はそこにあることができます-あなたは幸運になりました。他の誰かの本がそこにあり、あなたの本がホテルのかまどにある可能性があります。あなたが入って来たときに誰かがそこにいて、あなたの本をバラバラに引き裂く可能性があります。ホテルはテーブルと予約を完全に削除し、ワードローブと交換することができたはずです。ホテル全体が取り壊されてサッカースタジアムに置き換わる可能性があり、忍び寄っている間に爆発で死ぬことになります。

何が起こるかわかりません。ホテルをチェックアウトし、後で違法に使用するための鍵を盗んだとき、システムの規則に違反すること選択したため、予測可能で安全な世界に住む権利を放棄しました。

C++は安全な言語ではありません。それはあなたが元気にシステムのルールを破ることを可能にするでしょう。許可されていない部屋に戻って、もうそこにいないかもしれない机をくぐり抜けるなど、違法で愚かなことをしようとしても、C++はあなたを止めません。C ++よりも安全な言語は、パワーを制限することでこの問題を解決します。たとえば、キーをより厳密に制御できます。

アップデート

神聖な良さ、この答えは多くの注目を集めています。(理由はわかりません-私はそれを単なる「楽しい」小さなアナロジーだと考えましたが、何でもです。)

これをもう少し技術的な考えで更新するのは密接な関係があるのではないかと思いました。

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

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

2番目の方法は、各バイトの存続期間がよく知られている「短命」のストレージ領域を用意することです。ここでは、ライフタイムは「ネスト」パターンに従います。これらの短命の変数の中で最も長命の変数は、他の短命の変数の前に割り当てられ、最後に解放されます。寿命の短い変数は、寿命の長い変数の後に割り当てられ、それらの前に解放されます。これらの短命の変数の存続期間は、長命の変数の存続期間内に「ネスト」されます。

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

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

まるでホテルが順番に部屋を借りるだけで、部屋番号がチェックアウトするまでチェックアウトできないようなものです。

それでは、スタックについて考えてみましょう。多くのオペレーティングシステムでは、スレッドごとに1つのスタックを取得し、スタックは特定の固定サイズに割り当てられます。メソッドを呼び出すと、ものがスタックにプッシュされます。次に、元のポスターがここで行っているように、メソッドからスタックへのポインタを戻すと、それは完全に有効な100万バイトのメモリブロックの中央へのポインタにすぎません。私たちの例えでは、ホテルをチェックアウトします。あなたがそうするとき、あなたはちょうど最も大きい数の占有された部屋からチェックアウトしました。あなたの後に誰もチェックインせず、あなたが不法にあなたの部屋に戻った場合、あなたのすべてのものはこの特定のホテルにまだそこにあることが保証されます。

一時的な店舗には、本当に安くて簡単なスタックを使用しています。ローカルのストレージにスタックを使用するためにC++の実装は必要ありません。ヒープを使用できます。プログラムが遅くなるので、そうではありません。

C ++の実装では、スタックに残したゴミをそのままにしておく必要はありません。これにより、後で違法に戻ってくることができます。コンパイラが、空いたばかりの「部屋」のすべてをゼロに戻すコードを生成することは完全に合法です。繰り返しになりますが、それは高くつくからではありません。

スタックが論理的に縮小したときに、以前は有効だったアドレスが引き続きメモリにマップされるようにするために、C++の実装は必要ありません。実装では、オペレーティングシステムに「スタックのこのページの使用は終了しました。特に断りのない限り、以前に有効だったスタックページに誰かがアクセスするとプロセスを破棄する例外を発行します」と伝えることができます。繰り返しになりますが、実装は遅くて不必要であるため、実際にはそれを行いません。

代わりに、実装により、間違いを犯してそれを回避することができます。ほとんどの時間。ある日まで、本当にひどいことがうまくいかず、プロセスが爆発します。

これには問題があります。ルールはたくさんあり、誤って破るのはとても簡単です。私は確かに何度もあります。さらに悪いことに、この問題は、メモリが破損したことが発生してから数十億ナノ秒後にメモリが破損していることが検出された場合にのみ表面化することがよくあります。

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

さらに読むために:

  • C#が参照を返すことを許可した場合はどうなりますか?偶然にも、それは今日のブログ投稿の主題です:

    https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/

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

    https://ericlippert.com/tag/memory-management/

于 2011-06-22T20:01:23.010 に答える
283

ここで行っているのは、のアドレスであっaメモリの読み取りと書き込みです。の外にfooいるので、これはランダムなメモリ領域へのポインタにすぎません。あなたの例では、そのメモリ領域が存在し、現在それを使用しているものは他にありません。あなたはそれを使い続けることによって何も壊さず、そして他に何もまだそれを上書きしていません。したがって、5はまだそこにあります。実際のプログラムでは、そのメモリはほとんどすぐに再利用され、これを行うことで何かを壊してしまいます(ただし、症状はかなり後になるまで表示されない場合があります)。

から戻るとfoo、そのメモリを使用しなくなったことをOSに通知し、別のメモリに再割り当てできるようになります。運が良ければ、再割り当てされることはなく、OSがそれを再び使用していることに気付かない場合は、嘘をつきません。ただし、そのアドレスで終わるものは何でも上書きしてしまう可能性があります。

なぜコンパイラが文句を言わないのか疑問に思っているのなら、それはおそらくfoo最適化によって排除されたためです。通常、この種のことについて警告します。Cは、自分が何をしているのかを知っていることを前提としています。技術的には、ここではスコープに違反しておらず(a外部への参照はありませんfoo)、エラーではなく警告のみをトリガーするメモリアクセスルールのみです。

つまり、これは通常は機能しませんが、偶然に機能する場合もあります。

于 2011-06-23T05:43:54.023 に答える
155

まだ収納スペースが踏みにじられていなかったからです。その振る舞いを当てにしないでください。

于 2010-05-19T02:33:30.010 に答える
91

すべての答えへの少しの追加:

あなたがそのようなことをするなら:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

出力はおそらく次のようになります:7

これは、foo()から戻った後、スタックが解放され、boo()によって再利用されるためです。実行可能ファイルを分解すると、はっきりと表示されます。

于 2011-06-25T14:19:49.880 に答える
72

C ++では、任意のアドレスにアクセスできますが、アクセスする必要があるという意味ではありません。アクセスしているアドレスは無効になっています。fooが戻った後、他に何もメモリをスクランブルしなかったために機能しますが、多くの状況でクラッシュする可能性があります。Valgrindを使用してプログラムを分析するか、最適化してコンパイルしてみてください...

于 2011-06-22T14:15:01.713 に答える
69

無効なメモリにアクセスしてC++例外をスローすることはありません。あなたは、任意のメモリ位置を参照するという一般的な考え方の例を示しているだけです。私はこのように同じことをすることができます:

unsigned int q = 123456;

*(double*)(q) = 1.2;

ここでは、123456をdoubleのアドレスとして扱い、それに書き込みます。さまざまなことが起こる可能性があります。

  1. q実際には、実際にはdoubleの有効なアドレスである可能性がありますdouble p; q = &p;
  2. q割り当てられたメモリ内のどこかを指している可能性があり、そこで8バイトを上書きします。
  3. q割り当てられたメモリの外部を指し、オペレーティングシステムのメモリマネージャがセグメンテーション違反信号をプログラムに送信し、ランタイムがそれを終了させます。
  4. あなたは宝くじに当選します。

設定方法は、返されたアドレスがメモリの有効な領域を指している方が少し合理的です。スタックの少し下にある可能性がありますが、それでも無効な場所であり、決定論的なファッション。

通常のプログラム実行中に、そのようなメモリアドレスのセマンティック妥当性を自動的にチェックする人は誰もいません。ただし、などのメモリデバッガvalgrindはこれをうまく実行するため、プログラムを実行してエラーを確認する必要があります。

于 2011-06-22T14:15:11.937 に答える
29

オプティマイザーを有効にしてプログラムをコンパイルしましたか?関数は非常に単純であり、foo()結果のコードにインライン化または置換されている可能性があります。

しかし、結果として生じる動作が未定義であるというマークBに同意します。

于 2011-06-22T14:12:51.937 に答える
24

あなたの問題はスコープとは何の関係もありません。表示されているコードでは、関数mainは関数内の名前を認識していないため、外部でこの名前を使用してfooに直接fooアクセスすることはできません。afoo

あなたが抱えている問題は、不正なメモリを参照するときにプログラムがエラーを通知しない理由です。これは、C++標準では不正なメモリと正当なメモリの境界が明確に指定されていないためです。ポップアウトされたスタック内の何かを参照すると、エラーが発生する場合と発生しない場合があります。場合によります。この振る舞いを当てにしないでください。プログラム時に常にエラーが発生すると想定しますが、デバッグ時にエラーが発生することはないと想定します。

于 2011-06-23T04:45:29.920 に答える
21

すべての警告に注意してください。エラーを解決するだけではありません。
GCCはこの警告を示しています

警告:ローカル変数'a'のアドレスが返されました

これがC++の力です​​。あなたは記憶を気にする必要があります。フラグを使用する-Werrorと、この警告はエラーになり、デバッグする必要があります。

于 2015-08-17T06:30:16.290 に答える
18

メモリアドレスを返しているだけです。許可されていますが、おそらくエラーです。

はい、そのメモリアドレスを逆参照しようとすると、未定義の動作が発生します。

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
于 2010-05-19T02:33:39.557 に答える
18

スタックがそこに置かれてから(まだ)変更されていないため、これは機能します。再度アクセスする前に、他のいくつかの関数(他の関数も呼び出しています)を呼び出しaてください。おそらくもうそれほど幸運ではないでしょう...;-)

于 2011-06-23T15:31:51.980 に答える
18

これは、2日前ではなく、ここで説明されている古典的な未定義の動作です。サイト内を少し検索してください。一言で言えば、あなたは幸運でしたが、何かが起こった可能性があり、あなたのコードはメモリへの無効なアクセスを行っています。

于 2011-06-24T21:57:55.213 に答える
18

Alexが指摘したように、この動作は未定義です。実際、ほとんどのコンパイラは、クラッシュを発生させる簡単な方法であるため、これを実行しないように警告します。

発生する可能性のある不気味な動作の例については、次のサンプルを試してください。

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

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

これは「y=123」を出力しますが、結果は異なる場合があります(実際に!)。あなたのポインタは、他の無関係なローカル変数を破壊しています。

于 2011-06-24T22:04:21.467 に答える
16

あなたは実際に未定義の振る舞いを呼び出しました。

一時的な作品のアドレスを返しますが、関数の終わりに一時的なものが破棄されるため、それらにアクセスした結果は未定義になります。

したがって、変更せずに、かつてaのメモリ位置を変更しaました。この違いは、クラッシュする場合とクラッシュしない場合の違いと非常によく似ています。

于 2011-06-24T21:57:13.623 に答える
14

一般的なコンパイラの実装では、コードを「以前はaで占められていたアドレスを使用してメモリブロックの値を出力する」と考えることができます。また、ローカルを含む関数に新しい関数呼び出しを追加するintと、の値(または以前はポイントしaていたメモリアドレス)が変更される可能性が高くなります。aこれは、スタックが異なるデータを含む新しいフレームで上書きされるために発生します。

ただし、これは未定義の動作であり、動作するためにこれに依存するべきではありません。

于 2011-06-22T14:18:00.463 に答える
14

aは、そのスコープ(foo関数)の存続期間中に一時的に割り当てられる変数であるため、可能です。あなたがメモリから戻った後fooは空いていて、上書きすることができます。

あなたがしていることは未定義の振る舞いとして記述されています。結果を予測することはできません。

于 2011-06-24T21:57:54.787 に答える
12

:: printfを使用し、coutを使用しない場合、コンソール出力が正しい(?)ものは劇的に変化する可能性があります。以下のコード(x86、32ビット、MSVisual Studioでテスト済み)内でデバッガーを試すことができます。

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
于 2011-06-24T15:07:13.423 に答える
5

これは、メモリアドレスを使用する「ダーティ」な方法です。アドレス(ポインタ)を返すとき、それが関数のローカルスコープに属しているかどうかはわかりません。ただの住所です。'foo'関数を呼び出したので、' a'のそのアドレス(メモリ位置)は、アプリケーション(プロセス)の(少なくとも今のところ安全に)アドレス可能なメモリにすでに割り当てられています。'foo'関数が返された後、'a'のアドレスは'dirty'と見なすことができますが、そこにあり、クリーンアップされておらず、プログラムの他の部分(少なくともこの特定の場合)の式によって妨害/変更されていません。AC / C ++コンパイラは、このような「ダーティ」アクセスを阻止しません(ただし、気になる場合は警告が表示される場合があります)。

于 2017-03-08T15:25:53.130 に答える
5

関数から戻った後、メモリの場所に保持されている値の代わりにすべての識別子が破棄され、識別子がないと値を見つけることができませんが、その場所には以前の関数によって保存された値が含まれています。

したがって、ここで関数foo()はのアドレスを返し、そのアドレスaa返した後に破棄されます。そして、その返されたアドレスを介して変更された値にアクセスできます。

実際の例を見てみましょう。

男が場所にお金を隠し、場所を教えてくれたとしましょう。しばらくすると、お金の場所を教えてくれた男が亡くなります。しかし、それでもあなたはその隠されたお金にアクセスできます。

于 2017-07-19T07:07:55.830 に答える
1

あなたのコードは非常に危険です。ローカル変数(関数の終了後に破棄されたと見なされます)を作成し、破棄された後にその変数のメモリのアドレスを返します。

つまり、メモリアドレスが有効かどうかにかかわらず、コードはメモリアドレスの問題(セグメンテーション違反など)に対して脆弱になります。

これは、メモリアドレスをポインタに渡していて、まったく信頼できないため、非常に悪いことをしていることを意味します。

代わりに、この例を検討して、テストしてください。

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

あなたの例とは異なり、この例では次のようになります。

  • intのメモリをローカル関数に割り当てる
  • そのメモリアドレスは、関数の有効期限が切れても有効です(誰も削除しません)
  • メモリアドレスは信頼できます(そのメモリブロックは空きとは見なされないため、削除されるまで上書きされません)
  • 使用しない場合は、メモリアドレスを削除する必要があります。(プログラムの最後にある削除を参照してください)
于 2019-05-02T10:17:27.107 に答える
0

それは言語によって異なります。C&C ++ / Cppでは、はい、技術的には可能です。これは、特定のポインターが実際に有効な場所を指しているかどうかのチェックが非常に弱いためです。スコープ外の変数自体にアクセスしようとすると、コンパイラーはエラーを報告しますが、その変数の場所へのポインターを、まだ存在する他の変数に意図的にコピーしたかどうかを知るのは賢明ではないでしょう。後でスコープします。

ただし、変数がスコープ外になったらそのメモリを変更すると、まったく定義されていない影響があります。おそらくスタックが破損している可能性があります。スタックは、そのスペースを新しい変数に再利用している可能性があります。

JavaやC#などの最新の言語は、プログラマーが最初に変数の実際のアドレスにアクセスする必要がないようにするため、また、オブジェクトを指す変数の参照カウントを維持しながら、配列アクセスの境界チェックを行う必要がないようにするために、非常に長い時間がかかることがよくあります。ヒープ内で、それらが時期尚早に割り当て解除されないようにするなど。これはすべて、プログラマーが意図せずに安全でないことやスコープ内の変数の範囲外のことをしないようにすることを目的としています。

于 2021-05-02T06:44:35.650 に答える