ポインタを返す関数

ポインタの活用例その3

関数の戻り値には、ポインタを指定することもできます。
書き方はポインタ変数の時と同じく、関数名の前に間接演算子(*)を書きます。

以下のコードの関数Greetingは、引数に指定した時間(ただの整数)に応じて挨拶を返す関数です。
0~23の値を指定されることを想定しています。

#include <stdio.h>

char *Greeting(int time)
{
    if (time >= 5 && time < 11)
        return "おはよう。";
    if ((time >= 18 && time < 24) ||
        (time >= 0 && time < 5))
        return "こんばんは。";
    
    return "こんにちは。";
}

int main()
{
    int now = 11;
    printf("%s", Greeting(now));
    getchar();
}

関数の実行結果として返ってくるのは文字列ですが、文字列リテラルへのポインタで受け取ります。

関数内の処理は簡単です。
受け取った整数値に応じて、条件分岐で異なるreturn文を返しているだけです。
return文が実行されればそれ以降の関数内のコードは実行されませんから、else文を使用する必要はありません。

最後の「return "こんにちは。";」は、すべての条件を満たさなかった場合に実行されます。
これはどのような値が引数に指定されても、とりあえず何か挨拶を返すようにするためです。

return文をひとつにまとめる場合

コードの途中でreturn文を使用するとコードが読みづらくなるので、関数内にreturn文を複数用いるのは良くない、という人もいます。
(それでも上記のサンプルコードくらいは許容範囲だと思いますが)
その場合は、以下のようにできます。

#include <stdio.h>

char *Greeting(int time)
{
    char *ret;

    if (time >= 5 && time < 11)
        ret = "おはよう。";
    else if ((time >= 18 && time < 24) ||
        (time >= 0 && time < 5))
        ret = "こんばんは。";
    else
        ret = "こんにちは";

    return ret;
}

int main()
{
    int now = 11;
    printf("%s", Greeting(now));
    getchar();
}

あらかじめ戻り値として使用するポインタ変数を宣言しておきます。
そして条件分岐でそのポインタ変数に文字列へのポインタを代入します。

こうすれば、どのような条件であっても最後のreturn文が実行され、関数を抜けることができます。

ただし、最初に宣言したポインタ変数には必ず文字列へのポインタを代入してから戻り値を返すようにしてください。
条件設定が複雑になってくると、ポインタ変数を初期化しないまま関数を終了してしまうことがあり、バグの原因となる場合があります。

関数内で宣言した配列は返してはならない

以下のコードはダメなポインタの返し方の例です。

#include <stdio.h>

char *Greeting(int time)
{
    char mes1[] = "おはよう";
    char mes2* = "こんばんは";

    if (time >= 5 && time < 11)
        return mes1;
    if ((time >= 18 && time < 24) ||
        (time >= 0 && time < 5))
        return mes2;
    
    return "こんにちは。";
}

int main()
{
    int now = 11;
    printf("%s", Greeting(now));
    getchar();
}

関数Greetingにはreturn文が3つあります。

9行目は、関数内で宣言した文字列配列を返します。
11行目は、関数内で宣言した文字列へのポインタを返します。
13行目は、文字列リテラルをそのまま返します。

この中で、9行目のreturn文の返し方は意図しない動作となる可能性があります。
なぜならば、関数内で宣言された変数の寿命はその関数が終了するまで、という決まりがあるからです。
(変数のスコープを参照)
配列も変数の一種ですから同様です。

9行目で返されるのは、関数内で宣言された配列の先頭要素へのポインタです。
関数内で宣言された変数は、関数を抜けた後にもメモリ上にそのまま存在するという保障はどこにもありません。
つまり19行目のprintf関数で文字列を画面に表示しようとしても、すでに配列mes1はメモリ上に存在しない可能性があるのです。

11行目で返されるのは、関数内で宣言されたポインタ変数です。
ポインタ変数も、関数を抜ければメモリ上から破棄されるおそれがあります。
しかしこのポインタ変数が指すのは文字列リテラルで、関数が返すのはこの文字列リテラルを指すポインタです。
文字列リテラルはプログラムの実行中はメモリ上に存在することが保障されているので、アドレス情報に間違いがなければどこからでも問題なくアクセスできます。

同じ理由で、13行目で返されるのは文字列リテラルへのポインタですから、これも問題なくアクセスできます。

関数内で宣言した配列は戻り値にしてはいけないということは覚えておきましょう。

関数内で宣言した変数を返すのはOK

ちなみに、関数内で宣言した変数を返す場合は問題ありません。
(いままで普通にやってきたことです)
関数が終了すると変数も消滅しますが、return文は変数が消滅する前にその値をコピーしてそれを戻り値にするからです。
これは引数には常にコピーが渡されるのと同じです。

ただし、関数内で宣言した変数のアドレスを返してはいけません。
これは上の配列の説明と同じことです。
関数内で宣言した変数の寿命は関数が終了までなので、それ以降は存在することが保障されません。
存在しないデータにアクセスしようとするとエラーになります。

int *Func()
{
    int a = 10;

    //これはダメ
    return &a;
}

ちなみに、関数内で宣言した配列の要素を返す場合は問題ありません。
配列の要素を戻り値に指定すると、その要素をコピーしたものが戻り値にセットされるためです。
これは普通に変数を返すのと同じことです。

int Func()
{
	int arr[] = { 10,11, 12, 13 };

	//OK
	return arr[1];
}

ただし二次元配列の先頭要素は配列ですから、戻り値に指定できません。

int *Func()
{
	int arr[][3] = {
		{ 1, 2, 3 },
		{ 4, 5, 6 }
	};

	//ダメ
	return arr[1];
}