C言語のきほん|関数からポインタを返す方法と注意

関数の外でも安全に使えるポインタの返し方を、やさしくしっかり身につけよう

13章では、return文を使って整数値や浮動小数点値などの「値」を返す方法を学びました。
C言語では、こうした値だけでなく、ポインタを返すこともできます。

ポインタを返せるようになると、関数の中で用意したデータを呼び出し元で利用したり、関数どうしで同じデータを共有したりできるようになります。文字列処理や配列操作、動的メモリの利用など、実用的なプログラムを書くうえでもとても大切な考え方です。

ただし、関数からポインタを返すときには、強く意識しておかなければならないことがあります。
それは、返したポインタが指している先のメモリ領域が、関数終了後も本当に有効なのかどうか、という点です。

たとえば、関数の中で作ったローカル変数のアドレスを返してしまうと、見た目は動いているように見えても、あとで思わぬ不具合の原因になります。C言語では、このようなメモリの寿命を正しく理解することがとても重要です。

ここでは、まずローカル変数のアドレスを返してはいけない理由を確認し、そのうえで安全に返せる代表例として、静的変数のポインタを返す方法を見ていきます。ポインタに苦手意識がある方でも読み進めやすいように、表や図のイメージも交えながら丁寧に整理していきます。

関数からポインタを返すとはどういうことか

関数からポインタを返すとは、データそのものを返すのではなく、そのデータが置かれている場所の情報を返すことです。

たとえば、整数値 42 を返す場合は、そのまま 42 という値を返します。
一方で、ある変数のポインタを返す場合は、その変数がメモリ上のどこにあるか、つまりアドレスを返します。

違いを表で整理すると、次のようになります。

返すもの意味
データそのものを返す42
ポインタデータがある場所を返すint型変数のアドレス

値を返すだけなら、その値を受け取れば処理できます。
しかし、ポインタを返す場合は、そのポインタが指している先が今も使える状態かどうかを考えなければなりません。

この「指し先の寿命」を考えることが、関数からポインタを返すときのいちばん大切なポイントです。

ローカル変数のアドレスを返してはいけない理由

関数からポインタを返すとき、最初に必ず押さえておきたいのがこのルールです。

関数内のローカル変数のアドレスを返してはいけません。

ローカル変数には、次のような特徴があります。

性質内容
スコープ宣言されたブロックの中でだけ使える
寿命関数の実行中だけ存在し、終了すると使えなくなる

つまり、関数の中で作られたローカル変数は、関数が終わると寿命が尽きます。
そのため、そのアドレスを返してしまうと、呼び出し元ではすでに使えなくなった領域を指すポインタを受け取ることになります。

たとえば、次のようなコードは危険です。

ファイル名:14_3_1.c

#include <stdio.h>

int *bad_pointer(void);

int main(void)
{
    int *ptr = bad_pointer();

    printf("受け取った値は%dです。\n", *ptr);

    return 0;
}

int *bad_pointer(void)
{
    int value = 100;   /* 関数内だけで有効なローカル変数 */
    return &value;     /* ローカル変数のアドレスを返してしまっている */
}

このコードは、環境によってはたまたま 100 と表示されることがあります。
ですが、それは安全だからではありません。たまたまその場所に以前の値が残って見えているだけの可能性があります。

別の環境では違う値になったり、異常終了したりすることもあります。
このようなコードは未定義動作につながるため、絶対に避ける必要があります。

なぜ危険なのかをイメージで理解しよう

ローカル変数が危険な理由は、メモリの寿命をイメージするとわかりやすくなります。

関数の実行中は、ローカル変数のための領域が確保されています。
しかし、関数が終了すると、その領域はもうその変数のためには使われません。別の処理で上書きされたり、別の目的に使われたりする可能性があります。

つまり、ポインタ自体にはアドレスが残っていても、その先の中身はもう保証されません。

ここでは、次のように考えると理解しやすいです。

  • ポインタを返すこと自体が危険なのではない
  • 寿命が終わった領域を指すポインタを返すことが危険
  • ローカル変数は関数終了後に寿命が終わる

この3点をつなげて考えることが大切です。

安全に返せるポインタの代表例

では、どのような領域へのポインタなら安全に返せるのでしょうか。代表的なものを整理すると、次の4つがあります。

  • 静的変数のポインタ
  • 動的に確保されたメモリのポインタ
  • グローバル変数のポインタ
  • 引数で渡されたポインタ

表で見ると、次のようになります。

返せる対象安全な理由主な注意点
静的変数プログラム終了まで存在する同じ領域を使い回す
動的に確保したメモリ確保後も領域が残るfreeが必要
グローバル変数プログラム終了まで存在するどこからでも変更できる
引数で受け取ったポインタ呼び出し元が管理している有効期間は呼び出し元しだい

今回は、この中でも理解しやすく、基本として学びやすい「静的変数のポインタを返す方法」を中心に見ていきます。

図で見るメモリの寿命

この図では、関数の中にあるローカル変数と static 変数の違いを、見た目で比べられるようにしています。

ローカル変数は関数が終わると使えなくなるため、そのアドレスを返すのは危険です。
一方、static変数は関数を抜けたあとも残り続けるため、そのアドレスを返しても安全です。

つまり、関数からポインタを返せるかどうかは、その変数がどこに置かれているかではなく、いつまで有効かで判断することが大切です。

静的変数のポインタを返す方法

ここからは、静的変数のポインタを返す方法を具体的に見ていきます。

static で宣言された変数は、関数の中に書かれていても、普通のローカル変数とは寿命が異なります。
関数の処理が終わっても消えず、プログラムが終了するまで存在し続けます。

そのため、そのアドレスを返しても安全に使うことができます。

元のサンプルの内容を、よりシンプルな別のプログラム例に変えると、次のようになります。

ファイル名:14_3_2.c

#include <stdio.h>

int *get_message_count(void);

int main(void)
{
    int *count_ptr = get_message_count();

    printf("受け取った値は%dです。\n", *count_ptr);

    return 0;
}

int *get_message_count(void)
{
    static int count = 7;   /* 関数の外でも有効な静的変数 */
    return &count;          /* 静的変数のアドレスを返す */
}

実行結果例は次の通りです。

受け取った値は7です。

このプログラムでは、get_message_count 関数の中で static int count = 7; と宣言しています。
この count は関数内にある変数ですが、普通のローカル変数のように関数終了で消えるわけではありません。

そのため、return &count; でアドレスを返しても、main 関数でその値を安全に参照できます。

このプログラムの流れを順番に見てみよう

処理の流れを1つずつ整理すると、次のようになります。

手順内容
1main 関数から get_message_count を呼び出す
2関数内の静的変数 count を用意する
3count のアドレスを return で返す
4main 関数でそのアドレスを count_ptr に受け取る
5*count_ptr で値を取り出して表示する

ここで大切なのは、count_ptr に 7 が直接入っているわけではないということです。
count_ptr に入っているのは、count が保存されている場所のアドレスです。

そのため、実際の値を取り出すには、*count_ptr のように間接参照を行います。

static を付けると何が変わるのか

見た目はよく似ていますが、次の2つのコードは意味が大きく異なります。

まずは危険な例です。

int *func(void)
{
    int a = 10;
    return &a;
}

この a はローカル変数なので、関数が終わると寿命が尽きます。
そのため、返されたアドレスは無効になります。

次に安全な例です。

int *func(void)
{
    static int a = 10;
    return &a;
}

こちらの a は static変数なので、プログラム終了まで存在し続けます。
そのため、返されたアドレスは関数終了後も有効です。

このように、static が付くかどうかで、変数の寿命は大きく変わります。
ポインタを返す場面では、この違いをしっかり意識することが大切です。

静的変数のポインタを返す方法の長所

静的変数のポインタを返す方法には、次のような長所があります。

長所説明
実装が簡単malloc を使わずに書ける
解放が不要free を呼ぶ必要がない
関数終了後も有効呼び出し元でそのまま使える

学習用のサンプルや、小さな処理の動作確認では、とても扱いやすい方法です。
メモリ確保や解放の説明をまだ深くしていない段階でも、仕組みを理解しやすいのが大きな利点です。

静的変数のポインタを返す方法の注意点

便利な方法ですが、もちろん注意しなければならない点もあります。

同じ領域を使い回す

static変数は、関数を呼ぶたびに新しく作られるわけではありません。
同じ領域をずっと使い続けます。

そのため、関数を複数回呼んでも、返されるのは毎回同じ変数のアドレスです。

たとえば、次のような例があります。

ファイル名:14_3_3.c

#include <stdio.h>

int *next_number(void);

int main(void)
{
    int *p1 = next_number();
    int *p2 = next_number();

    printf("p1が指す値は%dです。\n", *p1);
    printf("p2が指す値は%dです。\n", *p2);

    return 0;
}

int *next_number(void)
{
    static int number = 0;   /* 同じ変数を使い続ける */
    number++;
    return &number;
}

このコードでは、p1 と p2 は別々の変数を指しているように見えるかもしれません。
ですが、実際にはどちらも同じ static変数 number を指しています。

そのため、あとから関数を呼ぶことで値が変わると、前に受け取ったポインタから見える内容も変わります。

再帰呼び出しに向いていない

再帰関数では、呼び出しごとに別々の状態を持ちたいことがあります。
しかし static変数はすべての呼び出しで共有されるため、再帰処理とは相性がよくない場合があります。

マルチスレッドでは競合の原因になる

複数のスレッドが同じ関数を同時に使うと、1つの static変数を共有して書き換えることになります。
そのため、競合や予期しない結果につながることがあります。

非リエントラントという考え方

少し発展的な話になりますが、static変数を内部で使う関数は、非リエントラントになりやすいです。

リエントラントとは、処理の途中で同じ関数がもう一度呼ばれても、安全に動作できる性質のことです。
static変数を共有していると、呼び出しごとに独立した作業領域を持てないため、この性質を満たしにくくなります。

最初のうちは、難しく考えすぎなくても大丈夫です。
まずは次のように覚えておくとよいです。

同じ static変数をみんなで使い回すので、複数回の呼び出しや同時実行には注意が必要。

この理解があれば十分です。

static変数は前回の状態を覚えられる

static変数の特徴は、危険性だけではありません。
うまく使うと、前回の状態を保持できるという便利さもあります。

たとえば、呼び出すたびに値が増えるような関数は、次のように書けます。

ファイル名:14_3_4.c

#include <stdio.h>

int *get_step(void);

int main(void)
{
    int *ptr;

    ptr = get_step();
    printf("1回目の値は%dです。\n", *ptr);

    ptr = get_step();
    printf("2回目の値は%dです。\n", *ptr);

    ptr = get_step();
    printf("3回目の値は%dです。\n", *ptr);

    return 0;
}

int *get_step(void)
{
    static int step = 0;   /* 前回の値を保持する */
    step++;
    return &step;
}

実行結果例は次の通りです。

1回目の値は1です。
2回目の値は2です。
3回目の値は3です。

この例では、step が関数終了後も保持されるため、前回の値を覚えたまま次の呼び出しに進めます。

このように、static変数には「関数の外でも有効」という性質だけでなく、「前回の状態を保持できる」という特徴もあります。
ただし、それは同じ領域を共有しているからこそ実現できるものです。便利さと注意点はセットで理解しておきましょう。

ほかの安全な返し方にも触れておこう

関数からポインタを返す方法は、static変数だけではありません。
今後の学習のために、ほかの代表的な方法も簡単に整理しておきます。

動的に確保したメモリを返す方法

malloc で確保したメモリは、関数が終わってもすぐには消えません。
そのため、そのアドレスを返すことができます。

ただし、使い終わったら free で解放しなければなりません。
この方法は柔軟ですが、解放忘れやメモリリークに注意が必要です。

グローバル変数のポインタを返す方法

グローバル変数はプログラム終了まで存在するため、そのアドレスを返すことができます。
ただし、どこからでも変更できるので、プログラム全体がわかりにくくなりやすいという弱点があります。

引数で受け取ったポインタを使う方法

呼び出し元が用意した領域を関数に渡し、その領域に書き込む方法もよく使われます。
この方法では、関数の中で新しく領域を管理しなくてよいため、比較的安全で実用的です。

比較すると、次のようになります。

方法特徴注意点
static変数を返す簡単で書きやすい同じ領域を共有する
mallocした領域を返す柔軟性が高いfreeが必要
グローバル変数を返す単純に書ける管理が難しくなる
引数のポインタを使う実用的で安全性が高い呼び出し側の準備が必要

よくある誤解に注意しよう

このテーマでは、いくつかの誤解が起こりやすいです。
整理しておくと、理解がぐっと安定します。

よくある誤解実際はどうか
一度動いたから安全たまたま動いただけの可能性がある
関数の中の変数は全部返せないstaticなら返せる
アドレスを返せば便利で安全指し先の寿命が残っている必要がある
staticなら何でも安心共有による問題がある

特に気をつけたいのは、「動いたから正しい」と考えてしまうことです。
C言語では、危険なコードでも、たまたま正しく見えてしまうことがあります。

そのため、実行結果だけではなく、そのメモリが本当に生きているかどうかを根拠に判断することが大切です。

学習するときに意識したいポイント

関数からポインタを返す問題では、毎回次の2つを考えると整理しやすくなります。

  • そのポインタは、どの領域を指しているか
  • その領域は、いつまで有効か

この2つを考えるだけで、危険なコードか安全なコードかをかなり判断しやすくなります。

たとえば、ローカル変数なら関数終了で無効です。
static変数ならプログラム終了まで有効です。
malloc で確保した領域なら free するまで有効です。

このように、ポインタを見るときは「値」ではなく「寿命」に注目すると、理解しやすくなります。

この図では、ローカル変数と static変数の寿命の違いを左右に並べて比較できます。
ローカル変数は関数が終わると消えるため、アドレスを返すのは危険です。
一方、static変数はプログラム終了まで残るため、アドレスを返すことができます。

このように、同じ「関数内の変数」でも、寿命の違いによって扱い方が変わることを図で確認できるようになります。

おわりに向けて押さえておきたいこと

関数からポインタを返すときに大切なのは、返した先のメモリ領域が関数終了後も有効であることです。
この条件を満たしていないと、見た目には動いていても、不安定で危険なプログラムになってしまいます。

ローカル変数のアドレスを返してはいけない理由は、変数の寿命が関数の中だけに限られているからです。
一方、static変数はプログラム終了まで存在するため、そのアドレスを返すことができます。

ただし、static変数は同じ領域を使い回すため、再帰呼び出しやマルチスレッドでは注意が必要です。
便利だからこそ、仕組みと注意点の両方をセットで理解しておくことが大切です。

ポインタを返す関数を見たときは、まず「そのポインタは何を指しているか」、そして「その領域はいつまで有効か」を考えるようにしてみてください。
この視点が身につくと、ポインタの理解がとても安定してきます。