C言語のきほん|安全に返せるポインタの種類

返してよいポインタ、気をつけるべきポインタを見分けて、安全な関数設計を身につけよう

C言語では、関数から整数や小数のような値だけでなく、ポインタを返すこともできます。
ポインタを返せるようになると、関数の中で用意したデータを呼び出し元で使ったり、複数の処理のあいだで同じメモリ領域を共有したりできるようになります。実用的なプログラムを書くうえで、とても大切な考え方です。

ただし、ポインタは「どこにデータがあるか」を表すものなので、何でも返してよいわけではありません。
関数から返したポインタが指している先のメモリ領域が、関数の終了後にもきちんと存在していることが大前提になります。ここをあいまいなままにしてしまうと、見た目は動いていても不安定なプログラムになったり、思わぬ不具合の原因になったりします。

前の内容では、ローカル変数のアドレスを返してはいけないことや、static変数のポインタなら返せることを見てきました。
ここでは、その続きとして、さらに代表的な「安全に返せるポインタの種類」を見ていきます。具体的には、mallocで動的に確保したメモリのポインタを返す方法と、グローバル変数のポインタを返す方法を取り上げます。

どちらも関数の外で利用できる有効な方法ですが、それぞれに便利な点と注意点があります。
大切なのは、返せるかどうかだけでなく、そのあと誰が管理するのか、どこで気をつけるべきなのかまで含めて理解することです。
このあたりを順番に整理しながら、やさしく確認していきましょう。

安全に返せるポインタとは何か

まず最初に、「安全に返せるポインタ」とは何かをはっきりさせておきましょう。

関数から返してよいポインタとは、関数を抜けたあとでも、その指し先が有効なまま残っているポインタのことです。

たとえば、ローカル変数は関数が終わると寿命が尽きるので、そのアドレスを返してはいけませんでした。
一方で、次のようなものは関数終了後も有効な場合があります。

返せる対象関数終了後も使える理由
static変数プログラム終了まで存在するため
mallocで確保したメモリfreeするまで残るため
グローバル変数プログラム終了まで存在するため
引数で渡されたポインタ呼び出し元が管理しているため

今回の中心は、このうち次の2つです。

  • mallocで動的に確保したメモリのポインタ
  • グローバル変数のポインタ

どちらも安全に返せる可能性がありますが、使い方を間違えると別の問題が起こります。
そのため、「返してよい」ことと「気軽に使ってよい」ことは同じではない、という意識も大切です。

動的確保されたメモリのポインタを返す方法

関数の中で malloc を使ってメモリを確保すると、その領域は関数を抜けても自動では消えません。
そのため、そのアドレスを返して、呼び出し元で使うことができます。

これは、関数の中で必要なサイズのデータ領域を用意し、その結果を呼び出し元に渡したいときによく使われる方法です。
配列を用意したいときや、入力内容に応じてサイズが変わるデータを扱いたいときに特に便利です。

ファイル名:14_4_1.c

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

int *make_scores(int count);

int main(void)
{
    const int num = 5;
    int *scores = make_scores(num);

    if (scores != NULL) {
        printf("作成した点数一覧: ");
        for (int i = 0; i < num; i++) {
            printf("%d ", scores[i]);
        }
        printf("\n");

        free(scores);   /* 使い終わったら必ず解放する */
    } else {
        printf("メモリを用意できませんでした。\n");
    }

    return 0;
}

int *make_scores(int count)
{
    int *p = malloc(sizeof(int) * count);   /* 必要な個数分の領域を確保する */

    if (p != NULL) {
        for (int i = 0; i < count; i++) {
            p[i] = (i + 1) * 10;   /* 10, 20, 30... を代入する */
        }
    }

    return p;   /* 確保した領域の先頭アドレスを返す */
}

実行結果例

作成した点数一覧: 10 20 30 40 50

この例では、make_scores 関数の中で必要な個数分の int 型の領域を確保し、その中に 10, 20, 30, 40, 50 という値を入れています。
そのあと、その先頭アドレスを返すことで、main 関数側で配列のように利用できるようにしています。

この方法が安全な理由

この方法が安全なのは、mallocで確保したメモリが関数終了と同時には消えないからです。

ローカル変数は関数が終わると寿命が終わりますが、mallocで確保したメモリは少し性質が違います。
その領域は、freeで明示的に解放するまで残り続けます。

つまり、次のような違いがあります。

種類関数終了後
ローカル変数使えなくなる
mallocで確保した領域freeするまで使える

この違いがあるため、mallocで確保した領域のアドレスは関数から返すことができます。

動的メモリを返すときの処理の流れ

この仕組みをしっかり理解するために、処理の流れを順番に整理してみましょう。

手順内容
1main 関数から make_scores を呼び出す
2関数内で malloc により必要な領域を確保する
3確保した領域に値を書き込む
4その先頭アドレスを return で返す
5呼び出し元でそのポインタを使って値を読む
6使い終わったら free で解放する

この最後の free がとても大切です。
ここを忘れると、確保したメモリが解放されず、メモリリークの原因になります。

動的メモリを返す方法の長所

mallocで確保したメモリを返す方法には、いくつかの大きな利点があります。

長所説明
サイズを柔軟に決められる実行時に必要な分だけ確保できる
関数の外でも使えるfreeするまでは有効だから
配列のように扱いやすい連続した領域を確保しやすい

たとえば、ユーザーの入力に応じて配列の長さを変えたい場合や、データ数が実行するまでわからない場合には、とても便利です。
固定サイズの配列では対応しにくい場面でも柔軟に使えます。

動的メモリを返すときの注意点

便利な方法ですが、注意点もかなり重要です。
ここを軽く見てしまうと、C言語らしいメモリ管理の難しさにすぐぶつかってしまいます。

NULLチェックが必要

mallocは、必ず成功するとは限りません。
メモリの確保に失敗した場合は NULL を返します。

そのため、呼び出し元では、返ってきたポインタが NULL ではないかを必ず確認する必要があります。

今回の例でも、次のように確認しています。

if (scores != NULL) {
    /* 正常に使う処理 */
} else {
    /* 失敗時の処理 */
}

この確認をせずにそのまま使ってしまうと、NULL を参照して異常終了することがあります。

freeによる解放が必要

mallocで確保したメモリは、自動では消えません。
そのため、使い終わったら free で解放する必要があります。

ここは static変数やグローバル変数との大きな違いです。
便利に使えるぶん、管理の責任が呼び出し側に移ります。

つまり、関数が領域を用意したなら、そのあと誰が free するのかをはっきりさせておくことが大切です。

メモリリークに注意

free を忘れると、不要になったメモリが解放されず、そのまま残ってしまいます。
これがメモリリークです。

小さなプログラムでは気づきにくいこともありますが、長時間動くプログラムや何度も同じ処理を繰り返すプログラムでは、少しずつメモリを消費して問題になります。

二重解放にも注意

同じポインタに対して free を2回行うのも危険です。
これは二重解放と呼ばれ、不正な動作の原因になります。

そのため、freeしたあとに必要ならポインタを NULL にしておく、という書き方もよく使われます。

動的メモリの話は、処理の流れを図で見るとかなり理解しやすくなります。

この図では、関数の中で malloc によって確保したメモリ領域が、return によって呼び出し元へ渡される流れを表しています。
ポイントは、返しているのが単なる値ではなく、「確保した領域の先頭アドレス」だということです。

また、右下に free を入れているのは、この方法では「使い終わったあとに必ず解放が必要」という点が重要だからです。
安全に返せる方法ではありますが、管理の手間もセットになっていることが、分かります。

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

次に、グローバル変数のポインタを返す方法を見ていきましょう。

グローバル変数は、関数の外で宣言され、プログラム全体から参照できる変数です。
そのため、プログラムが動いているあいだは存在し続けます。
この性質があるので、そのアドレスを関数から返しても、関数終了後に無効になることはありません。

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

ファイル名:14_4_2.c

#include <stdio.h>

int *select_level(int code);

int level_easy = 100;
int level_normal = 200;
int level_hard = 300;

int main(void)
{
    int *level_ptr = select_level(1);

    printf("選ばれた設定値は%dです。\n", *level_ptr);

    return 0;
}

int *select_level(int code)
{
    switch (code) {
        case 0:
            return &level_easy;
        case 1:
            return &level_normal;
        default:
            return &level_hard;
    }
}

実行結果例

選ばれた設定値は200です。

この例では、select_level 関数が引数 code の値に応じて、3つのグローバル変数のどれかのアドレスを返しています。
返されたポインタは、main 関数で受け取って使うことができます。

この方法が安全な理由

グローバル変数は、ローカル変数とは違って関数の中だけで生きているわけではありません。
プログラムの開始から終了まで存在するため、そのアドレスは関数を抜けたあとでも有効です。

つまり、次のように整理できます。

種類寿命
ローカル変数関数の実行中だけ
グローバル変数プログラム終了まで

このため、グローバル変数のアドレスを返すこと自体は安全です。

グローバル変数を返す方法の長所

グローバル変数のポインタを返す方法には、次のような長所があります。

長所説明
実装が単純すでにある変数のアドレスを返すだけでよい
寿命を気にしやすいプログラム終了まで残る
共有データとして使いやすい複数の関数からアクセスしやすい

設定値や共通状態など、プログラム全体で使うデータを扱うときには、考え方としてはわかりやすい方法です。

グローバル変数を返すときの注意点

ただし、グローバル変数は便利なぶん、設計上の注意がかなりあります。

どこからでも変更できる

グローバル変数は、プログラムのあちこちからアクセスできます。
それは便利でもありますが、逆に言えば、どこで値が変わったのか追いにくくなるということでもあります。

関数からポインタを返してしまうと、そのポインタを使って値を書き換えることもできるため、さらに管理が難しくなることがあります。

バグの原因を見つけにくくなる

ローカル変数なら、その関数の中だけを見ればある程度動作を追えます。
しかしグローバル変数は、別の関数、別のファイル、別の場所からも変更される可能性があります。

そのため、「いつのまにか値が変わっていた」という問題が起きやすくなります。

設計が複雑になりやすい

小さなプログラムでは便利でも、規模が大きくなるとグローバル変数に頼りすぎる設計は読みづらくなりがちです。
そのため、使用は必要最小限にとどめるのが基本です。

この図では、関数が新しい領域を作るのではなく、すでに存在しているグローバル変数の中から1つを選んで、そのアドレスを返していることがわかります。

動的メモリの場合とは違って、free は必要ありません。
その代わり、どこからでもアクセスできる共有データなので、管理のしやすさには十分注意が必要です。

動的メモリとグローバル変数の違いを比べてみよう

ここまで見てきた2つの方法には、共通点もあれば違いもあります。
表で整理すると、かなり見通しがよくなります。

項目mallocで確保したメモリグローバル変数
関数から返せるか返せる返せる
有効期間freeするまでプログラム終了まで
解放が必要か必要不要
サイズの柔軟性高い低い
管理の難しさ解放管理が必要変更箇所の管理が難しい

このように見ると、どちらも安全に返せる方法ではありますが、注意すべきポイントが違うことがわかります。

動的メモリは「解放の責任」に注意する必要があります。
グローバル変数は「共有しすぎによる管理の難しさ」に注意する必要があります。

学習のときに意識したい見方

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

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

さらに、今回の内容ではもう1つ加えておくとよいです。

  • その領域を誰が管理するのか

たとえば、mallocで確保した領域なら、使い終わったあとに誰が free するのかを意識する必要があります。
グローバル変数なら、誰が変更できるのか、どこで使われているのかを意識する必要があります。

この「寿命」と「管理」の2つを意識して読むと、ポインタを返す関数の理解がかなり深まります。

よくある誤解

このテーマでは、次のような誤解が起こりやすいです。

よくある誤解実際はどうか
mallocで確保した領域は自動で消えるfreeしない限り残る
グローバル変数は安全だから何でも使ってよい管理が難しくなりやすい
返せるポインタなら全部同じように扱える管理方法がそれぞれ違う
関数から配列を返すのは全部危険mallocで確保した領域なら返せる

このあたりは、値そのものではなく「メモリの性質」を意識すると整理しやすくなります。

安全に返せるポインタを使い分ける感覚

実際のプログラムでは、どの方法を使うかは目的によって変わります。

  • 必要なサイズをその場で決めたいなら、mallocで動的確保したメモリが向いている
  • 全体で共通して使う固定的なデータなら、グローバル変数が使われることもある
  • ただし、グローバル変数の使いすぎは避けたい
  • 動的メモリは便利だが、free忘れに注意する

つまり、「安全に返せる」というだけで選ぶのではなく、使いやすさと管理しやすさのバランスで選ぶことが大切です。

ポインタを返す関数を読むときも書くときも、
このポインタはいつまで生きているのか、そして誰が後始末をするのか、という視点を持つようにすると、とても理解しやすくなります。