質問: a_function(aa, bb); printf("%p", cc); printf("%d", *cc); } ``` a_functionおよびb_functionがありますが,a_functionは正しく関数内でmallocされたメモリへのアドレスを返却してくれます。 しかしb_functionは関数内で静的に宣言されているint型変数の参照は返却してくれませんでした。(返却以前にコンパイルエラーですが) このとき オライリー詳説Cポインタ https://www.oreilly.co.jp/books/9784873116563/ には関数内のスタックフレームは関数終了時にスタックからポップされるため関数内で宣言したアドレスが有効なものではなくなってしまうと記述がありました。 つまるところ, (1)a_functionが正しく目的のアドレスへとアクセスできるのはmallocで確保したint型のポインタが参照するメモリ分は関数を抜けても確保されたまま (呼び出し元でfreeする責任があるが)int型のポインタを返却しても呼び出し元でそこにアクセスできる....と認識しています。 しかし (2)b_functionでは関数内で宣言したint型(4byte)分のメモリは確保しても関数終了とともに開放される....と認識していますが 関数内で宣言した上記の変数 int c の参照自体は存在して返却できるのではないでしょうか? 上記コードa_functionでコンパイルが通り b_functionがコンパイルエラーになる点がイマイチピンと来ません。 単純に関数内で確保したアドレスを返したければmallocで動的にヒープへとメモリ確保したものを返却すればよさそうなのですが ちょっと喉に骨がひっかかったような感覚のため質問させてもらいました。 よろしくご教授お願いします。
回答: > > (2)b_functionでは関数内で宣言したint型(4byte)分のメモリは確保しても関数終了とともに開放される....と認識していますが > > 関数内で宣言した上記の変数 int c の参照自体は存在して返却できるのではないでしょうか? > > > 返却できたとして何が起こるか、試すためにmain()を書き換えました。 ``` int main (int c, char* param[]) { int *aptr, *bptr; bptr = b_function(3, 4); printf("bptr = %p, *bptr = %d\n", bptr, *bptr); aptr = a_function(1, 2); printf("aptr = %p, *aptr = %d\n", aptr, *aptr); printf("bptr = %p, *bptr = %d\n", bptr, *bptr); return 0; } ``` 私の手元に、これを試せるコンパイラがありました。Windows上の、やや古いコンパイラです。コンパイルして動作させた事例をお見せします。 ``` C:\Users\rubato\Documents> cl t87156.c Microsoft(R) 32-bit C/C++ Standard Compiler Version 13.00.9466 for 80x86 Copyright (C) Microsoft Corporation 1984-2001. All rights reserved. t87156.c t87156.c(15) : warning C4172: ローカル変数またはテンポラリのアドレスを返します。 Microsoft (R) Incremental Linker Version 7.00.9466 Copyright (C) Microsoft Corporation. All rights reserved. /out:t87156.exe t87156.obj C:\Users\rubato\Documents> t87156 bptr = 0018FE88, *bptr = 7 aptr = 003B0F10, *aptr = 3 bptr = 0018FE88, *bptr = 2130567168 ``` ご覧の通り、警告は出たものの、コンパイルは通り動作もした、しかし最初 "7" が正しく(?)表示されたけど、その後 2130567168 という値が表示されてしまいました。 int c として割り当てられたメモリ(この場合、0x18FE88番地)の値が**上書きされてしまった**のです(2130567168 == 0x7EFDE000 は何かのアドレスでしょう)。 ローカル変数のアドレスを返してはいけないのは、こうした現象が起こるからです。 int c に割り当てられたメモリは「解放」された後も存在し続けます。霧のように消えてなくなるわけでも、メモリがアクセスできなくなるわけでもなく、どこか別な場所に移動することももありません。従って、変数 c のアドレスもそのまま、仰る通り「参照自体は存在」し続けると言えなくありません。なので「喉に骨がひっかかったような感覚w」が残るのも、理解できなくありませんが、、、 メモリは、何らかの目的に合わせて・何らかの値を格納するもの・値を変化できるもの・です(ね?)。 では、メモリを**「解放する」とは**、どういうことでしょうか。ざっくり言えば、**他のコードが、他の目的に使って構わなくなる**という事ではないでしょうか。 ある時点で、もう値を残しておく必要がなくなった・・・ので、他の関数が・別の目的で使っても良いよ、という変数もあります。malloc() で取得したメモリをfree()で返却することが、まさにそうです。 関数内のローカル変数は、関数からリターンしたら、もう他の目的に使える、という前提でメモリを割り当てます。それがC言語の基本設計なのです。この、関数からリターンしたら解放する、ということを実現しているのがスタックです。関数のローカル変数はスタック領域(のメモリ)に割り当てられます。 > > オライリー詳説Cポインタ > > 関数内のスタックフレームは関数終了時にスタックからポップされるため > > 関数内で宣言したアドレスが有効なものではなくなってしまう > > > 繰り返しますが、アドレスが有効ではなくなるとは、int c に割当られたメモリが消えてなくなる...とかではなく、他のコード(上の事例では、おそらくa_function()関数)が、全く異なる目的に使うようになった、**もはや b_function() の int c ではなくなった**...だから上書きされたのです。 ※念の為:"7"と表示したprintf()関数も同じスタック・同じメモリを使って動作するので、その時点で int c を上書きしてしまい、最初から7と表示されない可能性もあります。ただ、上の事例では「たまたま・幸運にも」7という値が表示された、にすぎません。 > > b_functionは関数内で静的に宣言されているint型変数の参照は返却してくれません > > > ここの「静的に宣言されている」という言い回しは誤りです。なぜなら、C言語には static というキーワードがあるから。例えば、次のように宣言した変数 c であれば「静的に宣言されている」と言えます。 ``` int *b_function(int a, int b) { static int c; c = a + b; return &c; } ``` このように宣言された変数 c は、スタック領域ではないメモリ領域に割り当てられますので、関数からリターンした後も解放されることはありません。この使い方は定石とも言える正当なものであり、当然コンパイルも実行もできます。 > > (返却以前にコンパイルエラーですが) > > > お手元のコンパイラはエラーにしたわけですが、コンパイラによって扱いが異なるのですね。 実は私は最初、メモリが書き換えられる実行例をGCCでお見せできると思いました。試してみると、コンパイル時に警告は出たものの、コンパイルは通ったのですが、実行したら、何も表示せずセグメンテーション例外(!)で終了してしまいました。なんと、b_function()はNULLポインタを返していた、即ち0番地を返すコードが生成されていたのです。そこでWindows上のコンパイラを試した、という次第です。 エラーにするコンパイラ、コンパイルは通すけど実行時にエラーにするコンパイラ・・・NULLかどうか、チェックしてからアクセスしろってことでしょうが・・・それくらい、関数内部のローカル変数のアドレスを返すことは、C言語にとって尋常でない事だと言えます。