C言語でポインタ型を返却する関数でmallocした参照を返す場合,局所変数を返す場合の挙動の違い

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 1,632

jacky

score 38

C言語のポインタを返す関数の挙動でいま一つピンとこない点があったので
お聞きします。

例えば,以下のようなコードがあった場合

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>

int *a_function(int a, int b) {
    int* c = (int * )malloc(sizeof(int));
    *c = a + b;
    return c;
}

int *b_function(int a, int b) {
    int c;
    c = a + b;
    return &c;
}


int main (int c, char* param[]) {

    int aa = 1;
    int bb = 2;

    int *cc = NULL;
    cc = 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で動的にヒープへとメモリ確保したものを返却すればよさそうなのですが
ちょっと喉に骨がひっかかったような感覚のため質問させてもらいました。

よろしくご教授お願いします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • LouiS0616

    2017/08/06 00:54

    コンパイラは何をご利用でしょうか?私の環境(VS2015)ではコンパイルエラーは出ませんでした。

    キャンセル

  • jacky

    2017/08/06 01:00

    !失礼しました。コンパイラはMNgw32のgccです。

    キャンセル

回答 3

+2

こんにちは。

(2)b_functionでは関数内で宣言したint型(4byte)分のメモリは確保しても関数終了とともに開放される....と認識していますが
関数内で宣言した上記の変数 int c の参照自体は存在して返却できるのではないでしょうか?

その通りです。時々、見かけますね。

b_functionがコンパイルエラーになる

コンパイラによると思います。
危険なコードですのでgccやclangは警告を出してくれますが、コンパイルには通り、実行可能です。

サンプル

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

checkベストアンサー

+1

(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言語にとって尋常でない事だと言えます。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

(2)b_functionでは関数内で宣言したint型(4byte)分のメモリは確保しても関数終了とともに開放される....と認識していますが

その通りです。

関数内で宣言した上記の変数 int c の参照自体は存在して返却できるのではないでしょうか? 

存在しない変数のアドレス(参照)は無効です。「無効な参照が存在する」という事なら合っています。

b_functionがコンパイルエラー

エラーじゃなくて警告では?参照を返却は出来ますが、無効な値なので警告が出ます。無効な参照は返されても使い道が無いです。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/08/06 09:54

    「自動変数のアドレスを返り値にする」ことが、未定義の動作である「生存期間が終了したオブジェクトを指すポインタの値が使われる場合」に該当するのか少し判断がつきませんが、仮にそうだとすれば、未定義の動作である以上コンパイルエラーにしても規格には合致します。

    キャンセル

  • 2017/08/06 10:32

    返しただけでは、ポインタとして使われてはいないですね。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.21%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる
  • トップ
  • Cに関する質問
  • C言語でポインタ型を返却する関数でmallocした参照を返す場合,局所変数を返す場合の挙動の違い