このページでは、C言語において「関数から複数の値を返却する方法」について解説していきます。
まず断っておくと、C言語では関数から return
できる値は1つのみです。したがって、関数から複数の値を返却したい場合、return
以外の手段で値を返却する必要があります。その手段とは、具体的には「アドレスの利用」になります。
Contents
関数から複数の値を返却する方法
C言語では関数から return
できる値は1つだけではあるものの、関数の引数には複数の値を指定することが可能です。
引数に複数の値を指定することができることを利用し、引数に返却してもらいたい値を「格納してもらうためのメモリのアドレス」を指定するようにすることで、複数の値を返却する関数を実現することができます。
関数から複数の値を返却する場合の処理の流れ
まずは、関数から複数の値を返却する際の処理の流れについて解説していきます。
説明しやすくするため、ここからは具体的な関数例を用いて解説していきたいと思います。今回は、指定された2つの整数に対する「除算結果」と「剰余算結果」を返却する関数 func
を用いて解説していきます。
ただし、除算や剰余算は除数が 0
になると 0
除算が発生して実行することができません。そのため、0
除算が発生するなどして関数が正常に処理できない場合は、そのことを関数呼び出し側に伝えるために false
を返却するようにしたいと思います。逆に関数が正常に処理できた場合は true
を返却します。
つまり、この関数 func
では下記の3つの値を返却する必要があります。
- 関数処理結果(
true
orfalse
) - 除算結果(整数)
- 剰余算結果(整数)
このような関数 func
は、関数呼び出し側と関数 func
側とで次のような処理の流れを組むことによって実現することができます。
返却値を保存してもらうためのメモリを用意する
まず、関数呼び出し側は除算結果と剰余演算結果を関数 func
に保存してもらうための「メモリの確保」を行います。このメモリの確保は変数宣言を行うことで実現することができます。
関数処理結果用のメモリを確保していないのは、1つの値に関しては関数の return
により返却することが可能だからです。
つまり、関数 func
に保存してもらうためのメモリとして事前に確保しておく必要があるのは、関数から返却してもらいたい値の数 - 1
個分となります(関数の返却値の型を void
型にする場合は 関数から返却してもらいたい値の数
分のメモリが必要になります)。
用意したメモリのアドレスを引数で渡す
続いて、関数呼び出し側から関数 func
を実行します。この時、関数 func
の引数には、関数が処理を行うための引数(今回の場合は除算と剰余算を行うために必要になる値、すなわち被除数と除数の値)だけでなく、「事前に確保して用意したメモリのアドレス」も含めて引数指定を行います。
このメモリを変数宣言により確保した場合は、その宣言した変数の変数名の前に &
演算子を付加することで、その変数(メモリ)のアドレスを取得することが可能です。
このアドレスを引数で関数 func
に渡すことで、関数 func
はどのアドレスに結果を保存すれば良いかが分かるようになります。
引数で受け取ったアドレスに値を保存する
関数 func
が実行されたので、次は関数 func
の処理が行われることになります。もし 0
除算などが発生して処理できない場合は関数処理結果として false
を return
して関数を終了します。
処理が行える場合は、引数で渡された被除数と除数の値から除算結果と剰余算結果を計算します。そして、その2つの結果を「引数として受け取ったアドレスのメモリに保存」します。
アドレスからメモリに値を保存する際には、間接演算子 *
を利用して代入を実行します。
関数を終了する(return
して値を返却する)
メモリに結果を保存した後は、関数 func
は関数処理結果として true
を return
して関数を終了します。
関数が終了すれば関数呼び出し側に処理が戻ることになります。この時、関数呼び出し側は関数 func
の返却値として、まず関数処理結果(true
or false
)を受け取ることになります。
用意したメモリに保存された値を取得する
さらに、事前に用意した2つのメモリ(変数)には関数 func
によって保存された除算結果と剰余算結果が保存されていることになります。
すなわち、関数呼び出し側は関数 func
から下記の3つの値を受け取ったことになります。
- 関数処理結果(
true
orfalse
) - 除算結果(整数)
- 剰余算結果(整数)
後は、関数呼び出し側は受け取った値を取得し、その値を利用して処理を継続していけば良いことになります。
こんな感じで、アドレスを利用することで関数から実質的に複数の値を返却することができるようになります。
ですので、要は、複数の値を返却する関数を実現するためには、上記のような流れが実現できるように関数側の処理や関数呼び出し側の処理を実装してやれば良いことになります。
特に関数側の処理を実装する上でポイントになるのは下記の2つになります。
- 引数でアドレスを受け取る
- 受け取ったアドレスに返却したい値を保存する
あとは、関数呼び出し側でも下記に関しては注意が必要です。
- 返却値を受け取るためのメモリを用意する
ここからは、上記の3つに焦点を当てて解説していきたいと思います。
スポンサーリンク
引数でアドレスを受け取る
複数の値を返却する関数を実現するためには、まずは関数の引数でアドレスを受け取れるようにする必要があります。
C言語では、アドレスは基本的にポインタに格納しますので、関数の引数に、元々関数が処理を実行するのに必要な引数に加え、ポインタ引数も用意するようにします。
先程の例のように、return
する値に加えて除算と剰余算の結果を返却したい場合は、2つのポインタ引数を用意する必要があります。
例えば下記のように引数を用意すれば、ret_div
のアドレスのメモリに除算結果を、ret_mod
のアドレスのメモリに剰余算結果を保存することができるようになります。そしてこれにより、除算結果と剰余算結果を関数呼び出し側に渡すことができるようになります(もちろん関数呼び出し側から引数を適切に設定してもらう必要があります)。
bool func(int dividend, int devisor, int *ret_div, int *ret_mod) {
/* 略 */
}
dividend
と devisor
はそれぞれ割り算における被除数と除数(割り算は 被除数 / 除数
で行われる)を指定するためのもので、関数 func
が処理を行うために必要な値となります。
return
以外で返却したい値が2つあるので、これらの引数に加えて2つの結果返却用の引数 ret_div
と ret_mod
を用意するようにしています。もし、もっと多くの値を返却したいのであれば、返却したい値の個数に応じてポインタ引数を追加してやれば良いです。
ちなみにですが、上記では関数 func
の返却値の型を bool
にしていますが、この bool
型を利用するためには stdbool.h
をインクルードしておく必要があります。
この辺りの解説は下記ページで行っていますので、bool
型や true
・false
を利用したいような場合は是非読んでみてください。
受け取ったアドレスに返却したい値を保存する
続いて、引数で受け取ったアドレスのメモリに返却したい値を保存するようにしていきます。
ここでポイントになるのが、アドレスからメモリに値を保存する処理になります。変数からメモリに値を直接保存するのではなく、アドレスからメモリに値を保存する場合は、「間接演算子 *
」を利用する必要があります。
ポインタ引数にはアドレスが格納されているわけですから、ポインタ引数からメモリに値の保存を行う場合にも、この間接演算子 *
を利用する必要があります。
例えば、引数でアドレスを受け取る で用意したポインタ引数 ret_div
と ret_mod
に格納されたアドレスに引数で受け取った被除数と除数に対する除算結果・剰余算結果を保存するためには、下記のように間接演算子 *
を利用した上で値の代入を行う必要があります(false
を return
する場合の処理を省略しています。省略していない関数に関しては 複数の値を返却する関数の例 で紹介しています)。
bool func(int dividend, int devisor, int *ret_div, int *ret_mod) {
*ret_div = dividend / divisor;
*ret_mod = dividend % divisor;
return true;
}
正、下記のように間接演算子を利用せずに代入を行なった場合、ret_div
と ret_mod
の値が上書きされるだけであり、元々 ret_div
と ret_mod
に格納されていたアドレスのメモリへの値の保存は行われません。
bool func(int dividend, int devisor, int *ret_div, int *ret_mod) {
ret_div = dividend / divisor;
ret_mod = dividend % divisor;
return true;
}
何が起きているかを簡単に ret_div
の動作に注目して説明しておくと、まず関数実行時には、関数呼び出し側が事前に用意したメモリのアドレスがポインタ引数 ret_div
に格納された状態になります。つまり、ret_div
は関数呼び出し側が用意したメモリを指している状態になります。
それに対し、間接演算子を利用せずに直接 ret_div
に除算結果を代入した場合、ret_div
に「格納されているアドレス」が、その除算結果に上書きされます。つまり、ret_div
は除算結果の値をアドレスと考え、そのアドレスのメモリを指すことになります。例えば除算結果が3なのであれば、3番地のアドレスを指すことになります。
ポインタの指す先が変わるだけで、元々用意されていたメモリには何の値も保存されません。ですので、これだと関数呼び出し側に除算結果を渡すことができません。
その一方で、間接演算子を利用した場合、ret_div
そのものの値が上書きされるのではなく、ret_div
の「指すメモリ」に値が保存されることになります。
ですので、*ret_div
に除算結果を代入してやれば、ret_div
の指すメモリに除算結果を保存することができ、関数呼び出し側にも除算結果を渡すことができるようになります。
ポインタの場合、通常の変数と異なり代入の仕方が2通りあり(間接演算子の利用の有無の2通り)、間接演算子を利用しなかった場合はポインタの指す先(ポインタに格納されているアドレス)が変化し、間接演算子を利用した場合はポインタの指すメモリ(ポインタに格納されているアドレスのメモリ)が変化することになります。
この違いでプログラムの動作が大きく異なるので注意してください。
ちょっと解説が長くなりましたが、要はポインタ引数で結果を格納すべきメモリのアドレスが分かっているので、あとはポインタ引数で受け取ったアドレスに対して間接演算子 *
を利用してメモリへの結果の保存を行えば、関数呼び出し側に対してメモリを介して結果を渡すことができます。
返却値を受け取るためのメモリを用意する
複数の値を返却可能な関数の実現方法についての解説は以上になります。
次に、関数呼び出し側で行う結果保存用のメモリの用意について解説しておきます。
返却値を保存してもらうためのメモリを用意する で解説したように、C言語においては変数宣言自体がメモリの用意であると考えて良いです(変数宣言を行うことで変数が適切な位置のメモリに自動的に配置されるため、メモリが用意されたと考えられる)。
ですので、前述の func
関数を実行するのであれば、下記のように2つの変数の変数宣言を行い、それらの変数のアドレスを関数の引数に指定してやれば良いです。変数のアドレスは、アドレス演算子 &
により取得することができます。
int val_div, val_mod;
func(10, 3, &val_div, &val_mod);
func
関数が上手く作られていれば、関数実行後は val_div
と val_mod
に func
関数が返却した値が格納された状態になります。
ただし、関数の引数がポインタであるからといって、事前に変数宣言する変数をポインタ変数にすると上手く動作しなくなる可能性があるので注意してください。
例えば下記のように変数宣言を行なって func
関数を実行した場合、func
関数は意図した通りに動作してくれないはずです。
int *p_val_div, *p_val_mod;
func(10, 3, p_val_div, p_val_mod);
上記の2つのパターンでどのように動作が異なるのかについて解説しておきます(説明を簡単にするため val_div
のみに注目して解説していきますが、val_mod
に関しても同様のことが言えます)。
前者の場合、func
関数の引数には「変数 val_div
のアドレス」が指定されることになります。val_div
は当然変数宣言されていますので、val_div
用のメモリが用意された状態で、そのメモリのアドレスが func
関数に引数 ret_div
として渡されることになります。
func
関数実行直後の引数 ret_div
の様子を表したのが下の図となります。引数 ret_div
には val_div
のアドレスが渡されますので、ret_div
は val_div
のメモリを指していることになります。
ですので、間接演算子を利用して ret_div
からメモリに値を保存すれば、関数呼び出し側で用意したメモリである val_div
に値が格納されることになります。
その一方で後者の場合、func
関数の引数 ret_div
には変数 p_val_div
に格納されている値が渡されることになります。p_val_div
はポインタ変数ですので、格納されている値もアドレスとして扱われますが、p_val_div
には初期値を格納していませんので func
関数に渡されるアドレスは不定アドレスとなります(どこ指しているか分からないアドレス)。
そのため、func
関数実行直後の ret_div
の様子を表すと下の図のような感じになります。要は、p_val_div
自体のアドレスではなく、p_val_div
に格納されているアドレスが ret_div
にも格納されることになります。
p_val_div
は事前に用意したメモリのアドレスが格納されているわけではありませんので、そのアドレスのメモリに ret_div
から除算結果を保存しようとすると、メモリアクセス違反が発生してプログラムが強制終了させられることになります(事前に用意したメモリにアクセスするとメモリアクセス違反が発生する)。
引数で不定アドレスを渡していることも問題ではあるのですが、ここでの1番の問題は「事前に用意したメモリ」のアドレスを func
関数に渡せていないところです(用意したメモリは “確保したメモリ”・”予約したメモリ” と言った方が分かりやすいかもしれません)。
前者のパターンでは変数宣言により事前に用意したメモリのアドレスを func
関数に渡せていますが、後者のパターンでは事前に用意したメモリのアドレスが func
関数に渡せていません(しかも不定アクセス)。
関数側で関数呼び出し側に渡すための値を保存してもらうためには、事前に関数呼び出し側で用意したメモリのアドレスを関数に渡す必要があります。
逆に変数宣言をポインタ変数として行なったとしても、そのポインタ変数に事前に用意したメモリのアドレスを格納しておけば、上手く動作させることができます。
例えば、下記のように別途変数宣言を行なった変数のアドレスを事前に格納しておけば、変数宣言により用意されたメモリのアドレスを func
関数に渡せるので、正常に func
関数を動作させることができます。
int *p_val_div, *p_val_mod;
int val_div, val_mod;
p_val_div = &val_div;
p_val_mod = &val_mod;
func(10, 3, p_val_div, p_val_mod);
上記の場合、func
関数実行後に val_div
と val_mod
に func
関数が返却した値が格納されることになります(*p_val_div
と *p_val_mod
から取得するのでも良い)。
また、下記のように malloc
関数で確保したメモリのアドレスを func
関数に渡すのでも良いです。
int *p_val_div, *p_val_mod;
p_val_div = malloc(sizeof(int));
p_val_mod = malloc(sizeof(int));
func(10, 3, p_val_div, p_val_mod);
free(p_val_div);
free(p_val_mod);
上記の場合、func
関数実行後に *p_val_div
と *p_val_mod
に func
関数が返却した値が格納されることになります。
ただし、malloc
関数で確保したメモリは使い終わったら free
する必要があるので注意してください。
この malloc
関数については下記ページで解説していますので、詳しくは下記ページを参考にしていただければと思います。
スポンサーリンク
複数の値を返却する関数の例
ここまでの解説にも利用してきた、引数で被除数と除数を受け取って関数処理結果(true
or false
)・除算結果(整数)・剰余算結果(整数)の3つの値を返却する関数 func
を利用するプログラムの全体のソースコードを紹介しておきます。
#include <stdio.h>
#include <stdbool.h>
bool func(int dividend, int divisor, int *ret_div, int *ret_mod) {
if (divisor == 0) {
return false;
}
if (ret_div == NULL || ret_mod == NULL) {
return false;
}
*ret_div = dividend / divisor;
*ret_mod = dividend % divisor;
return true;
}
int main(void) {
int val_div, val_mod;
int x, y;
printf("dividend:");
scanf("%d", &x);
printf("divisor:");
scanf("%d", &y);
if (func(x, y, &val_div, &val_mod)) {
printf("div = %d, mod = %d\n", val_div, val_mod);
} else {
printf("divisor is zero...\n");
}
return 0;
}
わざわざ引数にアドレスを指定する理由
さて、ここまで複数の値を返却する関数を実現するためには、引数としてアドレスを受け取れるようにする必要があることを前提にして解説してきました。
なぜ、わざわざアドレスを受け取れるようにする必要があるのでしょうか?
例えば、複数の値を返却する関数の例 で紹介したソースコードを下記のように変更し、関数 func
でアドレスを受け取らないようにした場合でも上手く動作してくれそうではないでしょうか?
#include <stdio.h>
#include <stdbool.h>
bool func(int dividend, int divisor, int ret_div, int ret_mod) {
if (divisor == 0) {
return false;
}
ret_div = dividend / divisor;
ret_mod = dividend % divisor;
return true;
}
int main(void) {
int val_div, val_mod;
int x, y;
printf("dividend:");
scanf("%d", &x);
printf("divisor:");
scanf("%d", &y);
if (func(x, y, val_div, val_mod)) {
printf("div = %d, mod = %d\n", val_div, val_mod);
} else {
printf("divisor is zero...\n");
}
return 0;
}
結論としては、上記の func
関数では1つの値しか返却することはできません。
C言語においては、関数呼び出し時の引数の渡し方は「値渡し」となっています。
値渡しとは、関数呼び出し時に指定した変数の値のみがコピーされて関数の引数に渡される方式の「引数の渡し方」です(他プログラミング言語には。引数の渡し方が「参照渡し」となっているものもあります)。
つまり、関数には関数呼び出し時に指定した引数の値が渡されますが、関数呼び出し時に指定した変数と関数の引数とは全く別物です。ですので、関数の引数を変更したとしても、関数呼び出し時に指定した変数には全く影響を及ぼしません。
そのため、上記ソースコードのように関数の中で引数の値を変更したとしても、関数呼び出し側に値を返却することにはなりません。
ですが、コピーされるのが値であったとしても、関数呼び出し側で用意した変数等のメモリのアドレス値を関数に渡した場合、関数側は関数呼び出し側の変数等のメモリのアドレスを知ることができます。
そして、そのアドレスを知っていれば、関数側から関数呼び出し側の変数等のメモリにアクセスすることができます(関数側で間接演算子を利用すれば)。
そのため、関数呼び出し側で使用しているメモリや変数を結果的に変更するようなことができます。例えば、上の図において関数 func
内で *c = 50
を実行すれば、関数呼び出し元で使用している変数 z
の値が 50
に変化します。
さらに、これを利用することで複数の値を関数呼び出し側に返却することが可能になります。ですので、複数の値を返却する関数を実現するためには、引数でアドレスを受け取れるようにしてやる必要があります。
まとめ
このページでは、C言語における「関数から複数の値を返却する方法」について解説しました!
C言語 において関数から return
できる値は1つのみですが、関数呼び出し元で事前にメモリを用意して引数でそのメモリのアドレスを渡すようにすることで、複数の値を関数から返却できるようになります。
関数から返却できる値が1つのみだと不便なことは結構多いですし、複数の値を返却したいような場合は今回紹介した流れや関数の作り方を参考にしていただければと思います。
特に関数を作る際には引数にアドレスを持たせるようにすることと間接演算子を利用するあたりがポイントになるかと思います。アドレスやポインタの勉強にもなると思いますので、是非「複数の値を返却する関数」の作成に挑戦してみてください!
オススメの参考書(PR)
C言語学習中だけど分からないことが多くて挫折しそう...という方には、下記の「スッキリわかるC言語入門」がオススメです!
まず学習を進める上で、参考書は2冊持っておくことをオススメします。この理由は下記の2つです。
- 参考書によって、解説の仕方は異なる
- 読み手によって、理解しやすい解説の仕方は異なる
ある人の説明聞いても理解できなかったけど、他の人からちょっと違った観点での説明を聞いて「あー、そういうことね!」って簡単に理解できた経験をお持ちの方も多いのではないでしょうか?
それと同じで、1冊の参考書を読んで理解できない事も、他の参考書とは異なる内容の解説を読むことで理解できる可能性があります。
なので、参考書は2冊持っておいた方が学習時に挫折しにくいというのが私の考えです。
特に上記の「スッキリわかるC言語入門」は、他の参考書とは違った切り口での解説が豊富で、他の参考書で理解できなかった内容に対して違った観点での解説を読むことができ、オススメです。題名の通り「なぜそうなるのか?」がスッキリ理解できるような解説内容にもなっており、C言語入門書としてもかなり分かりやすい参考書だと思います。
もちろんネット等でも色んな観点からの解説を読むことが出来ますので、分からない点は別の人・別の参考書の解説を読んで解決していきましょう!もちろん私のサイトも参考にしていただけると嬉しいです!
入門用のオススメ参考書は下記ページでも紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/