3

関数呼び出しを理解しようとすると、簡単なコードを記述します。しかし、私はそれが出力されていることを理解できません。

#include <stdio.h>

int* foo(int n)
{
    int *p = &n;
    return p;
}

int f(int m)
{
    int n = 1;
    return 999;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(num);
    int q = f(999);
    printf("[%d]\n[%d]\n", *p, q);
    /* printf("[%d]\n", *q); */
}

出力:

[999]
[999]

なぜ*p999なのですか?

次に、次のようにコードを変更しました。

#include <stdio.h>

int* foo(int n)
{
    int *p = &n;
    return p;
}

int f()
{
    int n = 1;
    return 999;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(num);
    int q = f();
    printf("[%d]\n[%d]\n", *p, q);
    /* printf("[%d]\n", *q); */
}

出力:

[1]
[999]

なぜ*pここに1があるのですか?私はLinuxでgccを使用していますが、Clangは同じ出力を取得しました。

4

6 に答える 6

4

スタック変数へのポインターを返すためにコードが未定義の動作を引き起こしているという事実は別として、f() の署名を変更すると動作が変わる理由を尋ねていました。

その理由

その理由は、コンパイラが関数のスタックフレームを構築する方法にあります。コンパイラが foo() に対して次のようにスタック フレームを構築しているとします。

Address Contents  
0x199   local variable p
0x200   Saved register A that gets overwritten in this function
0x201   parameter n
0x202   return value
0x203   return address

f(int m) の場合、スタックは同様に静かに見えます。

Address Contents  
0x199   local variable n
0x200   Saved register A that gets overwritten in this function
0x201   parameter m
0x202   return value
0x203   return address

では、foo の 'n' へのポインターを返すとどうなるでしょうか。結果のポインタは 0x201 になります。foo を返すと、スタックのトップは 0x204 になります。メモリは変更されず、値「1」を読み取ることができます。これは、別の関数を呼び出すまで機能します(あなたの場合は「f」)。f を呼び出した後、位置 0x201 はパラメーター m の値で上書きされます。

この場所にアクセスすると (そして printf ステートメントでアクセスすると)、「999」と表示されます。f() を呼び出す前にこの場所の値をコピーしていた場合、値 '1' が見つかります。

この例に固執すると、パラメータが指定されていないため、f() のスタックフレームは次のようになります。

Address Contents  
0x200   local variable n
0x201   Saved register A that gets overwritten in this function
0x202   return value
0x203   return address

'1' でローカル変数を初期化しているので、f() を呼び出した後、位置 0x200 で '1' を読み取ることができます。ここで位置 0x201 から値を読み取ると、保存されたレジスタの内容が取得されます。

いくつかの追加ステートメント

  • 上記の説明は、観察するものを観察する理由を示す方法論を示すことであることを理解することが重要です。
  • 実際の動作は、使用しているツールチェーンと、いわゆる呼び出し規約によって異なります。
  • 何が起こるかを予測するのが難しい場合があることは容易に想像できます。これは、メモリを解放した後にメモリにアクセスするのと同様の静かな状況です。そのため、一般的に何が起こるかは予測できません。
  • この動作は、最適化レベルを変更することで変更することもできます。たとえば、-O3 をオンにすると、使用されていない変数 n がバイナリに表示されなくなるため、観察結果が異なると想像できます。
  • 背後にあるメカニズムを理解すれば、foo から取得したアドレスへの書き込みアクセスが深刻な問題につながる理由が理解できるはずです。

この説明を実験で証明しようとする勇者のために

まず、上記の説明が実際のスタック フレーム レイアウトに依存していないことを確認することが重要です。わかりやすいイラストにするために、レイアウトを紹介しました。

自分のマシンで動作をテストする場合は、お気に入りのデバッガーを使用して、ローカル変数とパラメーターが配置されているアドレスを調べて、実際に何が起こるかを確認することをお勧めします。注意: f の署名を変更すると、スタックに配置される情報が変更されます。したがって、唯一の実際の「移植可能な」テストは、f() のパラメーターを変更し、p が指す値の出力を観察することです。

f(void) を呼び出す場合、スタックに置かれる情報は大幅に異なり、p が指している位置に書き込まれる値は、パラメーターやローカル変数に必ずしも依存しなくなります。また、メイン関数のスタック変数に依存することもあります。

たとえば、私のマシンでは、2 番目のバリアントで読み取った '1' は、n のロードに使用されているように見えるため、'1' を格納するために使用されたレジスタを "num" に保存した結果であることが再現によって明らかになりました。

これがあなたにいくつかの洞察を与えることを願っています。さらに質問がある場合は、コメントを残してください。(これは理解するのが少し奇妙であることを知っています)

于 2013-01-25T14:59:42.307 に答える
2

未定義の動作を呼び出しています。ローカル変数のアドレス(この場合は引数int n)を返し、後で役立つことを期待することはできません。

于 2013-01-25T14:20:53.227 に答える
2

nここのコードのようなローカル変数:

int* foo(int n)
{
    int *p = &n;
    return p;
}

foo関数が終了するとすぐに「消えます」 。

その変数にアクセスすると、予期しない結果が生じる可能性があるため、使用できません。ただし、次のように書くこともできます。

int* foo(int* n)
{
    *n = 999;
    return p;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(&num);
    printf("[%d]\n", *p);
}

num印刷の時点で変数がまだ存在するためです。

于 2013-01-25T14:24:19.597 に答える
0

この未定義の動作は、スタックの関与によるものです

int *p = foo(num);
int q = f(999);

最初のケースでは、 と言うと&num、実際にはアドレスが格納されていたスタックnumに格納されます。次に、foo(num) が実行を完了し、f(999) がパラメーター 999 で動作します。同じスタックが使用されるため、num が格納されたスタック内の同じ場所にパラメーター 999 が割り当てられます。スタックが連続していることがわかります。 .

これが両方の印刷の理由です999。実際には、どちらもスタック内の同じ場所の内容を出力しようとします。

一方、2 番目のケースでは、f() にパラメーターが渡されないため、num は上書きされません。したがって、これは期待どおりに出力されます。

于 2013-01-25T16:34:21.400 に答える
0

最初のサンプルでは、

int num = 1;
int *p = foo(num);

どこfoo()ですか

int* foo(int n)
{
    int *p = &n;
    return p;
}

変数numfrommain()が渡されると、値によって に渡されますfoonumつまり、という変数のコピーがnスタック上に作成されます。numとのn値は同じですが、変数が異なるため、アドレスが異なります。

pから戻るとfoo()、 はで遅延されmain()たアドレスとは異なるアドレスの値を取得します。nummain()

同じ説明が、変更されたプログラムにも当てはまります。

明確にするために、別の例を見てみましょう。

int i = 2;

int * foo()
{
return &i;
}

int main() {

i = 1;
int *p = foo();
return 0;

}

この場合、iはヒープ上で宣言され、同じものiが と の両方で参照されmain()ますfoo()。同じアドレスと同じ値。

3 番目の例を見てみましょう。

int i = 2;

int * foo(int i)
{
return &i;
}

int main() {

int i = 1;
int *p = foo(i);
return 0;

}

ここでは、 global がありますがi、 のローカル変数によって隠され、それがimain()渡されfoo()ます。したがって、&iから返されるfoo、つまりpinの値はmain()、main() で宣言された変数 i のアドレスとは異なります。

これにより、変数のスコープと値渡しが明確になることを願っています。

于 2013-01-25T14:32:36.483 に答える
0

アセンブラの出力がなければ簡単ではありませんが、これは私の推測です:

ローカルとパラメーターはスタックに格納されます。したがって、 を呼び出すとfoo、スタックにある最初のパラメーターのアドレスが返されます。

最初の例では、2 番目の関数にパラメーターを渡します。これは、スタックの正確な場所にプッシュされますp。したがって、 の値を上書きします*p

2 番目の例では、スタックは 2 番目の呼び出しで変更されません。古い値 (のnum) はそこに残ります。

于 2013-01-25T14:53:28.877 に答える