ポインタを返す関数
ポインタの活用例その3
関数の戻り値には、ポインタを指定することもできます。
書き方はポインタ変数の時と同じく、関数名の前(データ型の後)に間接演算子(*
)を書きます。
以下のコードの関数Greeting
は、引数に指定した時間(ただの整数)に応じて挨拶を返す関数です。
0~23の値を渡されることを想定しています。
#include <stdio.h>
const 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;
const char* ret = Greeting(now);
printf("%s", ret);
getchar();
}
return文に指定しているのは文字列リテラルですが、文字列リテラルを評価して得られるのは文字列へのポインタですので、char*型が返されます。
関数内の処理は簡単です。
受け取った整数値に応じて、条件分岐で異なるreturn文を返しているだけです。
return文が実行されるとそれ以降の関数内のコードは実行されませんから、else文を使用する必要はありません。
最後のreturn "こんにちは。";
は、すべての条件を満たさなかった場合に実行されます。
これはどのような値が引数に指定されても、とりあえず何か挨拶を返すようにするためです。
return文をひとつにまとめる場合
コードの途中でreturn文を使用するとコードが読みづらくなるので、関数内にreturn文を複数用いるのは良くない、という人もいます。
(それでも上記のサンプルコードくらいは許容範囲だと思いますが)
その場合は、以下のようにできます。
#include <stdio.h>
const char *Greeting(int time)
{
const 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>
const char* Greeting(int time)
{
const char mes1[] = "おはよう";
const 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行目で返されるのは、関数内で宣言されたポインタ変数です。
ポインタ変数も、関数を抜ければメモリ上から破棄されるおそれがあります。
しかしこのreturn文が返すのは「ポインタ変数の値のコピー」です。
(関数の引数や戻り値には変数の値のコピーが渡される)
ポインタ変数の値はアドレスで、この場合は文字列リテラルのアドレスです。
文字列リテラルはプログラムの実行中はメモリ上に存在することが保証されているので、アドレス情報に間違いがなければどこからでも問題なくアクセスできます。
同じ理由で、13行目で返されるのは文字列リテラルのポインタですから、これも問題なくアクセスできます。
要するに関数内で宣言した配列は戻り値にしてはいけないということは覚えておきましょう。
文字列の配列を返すのも、文字列のポインタ変数を返すのも、どちらもアドレスを返すのだから同じことをしているように見えるかもしれません。
しかし、文字列リテラルによるchar型配列の初期化は「文字列の長さ分のメモリ領域を確保し、そこに文字列リテラルを一文字ずつコピーする」という処理です。
(ポインタと文字列を参照)
文字列配列へのポインタはその配列が確保しているメモリ領域を指すものであり、文字列リテラルを指しているわけではありません。
関数の呼び出し終了の時点でそのメモリ領域は破棄される可能性があるので、不正なメモリ領域への参照となってしまうのです。
関数内で宣言した変数を返すのは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];
}