配列を関数に渡す

こんにちは、めのんです!

前回は配列のさまざまな特性について解説しました。
配列についてはまだまだ解説しないといけないことがありますので今回もその中に含まれます。

今回解説するのは配列を関数に渡す方法についてです。
ちょっとややこしい話になりますので、必ずご自身でも手を動かしながらひとつひとつ確認していただければ幸いです。

前回、前々回と配列が先頭要素へのポインタに暗黙的に変換されることを解説しました。
今回もこの振る舞いが重要になりますので、まだ理解があやふやな方はおさらいしておいた方がいいでしょうね。

配列型の仮引数

以前解説したように関数の返却値の型に配列型を指定することはできません。
一方で関数の仮引数であれば配列を指定することができます。
引数に配列を指定することはできるのですが、仮引数に書いた配列は実際にはその配列要素の型へのポインタとして振る舞うことになります。

具体例を見ていきましょう。

#include <stdio.h>

void func(int a[3])
{
  printf("%zu\n", sizeof(a));
}

int main(void)
{
  int a[] = { 1, 2, 3 };

  func(a);
  printf("%zu\n", sizeof(a));
  return 0;
}

上のサンプルコードでは、main関数の中でfunc関数に実引数として渡したaも、func関数が仮引数として受け取ったaもどちらもint型の要素を3つ持つ配列として宣言されていますね。

普通に考えると、main関数の中に書いたprintf関数の出力結果も、func関数の中に書いたprintf関数の出力結果も同じになるはずです。
ここはぜひ実際にご自身で試してみていただきたいのですが、結果は全然違うはずです。

int型が4バイトの環境であれば、main関数の中のprintf関数は4×3で12を出力することになります。
ところがfunc関数の中のprintf関数は、典型的な64ビット環境では8を、同じく32ビット環境では4を出力することになると思います。

これはどういうことかというと、func関数の仮引数aは配列として宣言したにもかかわらず、実際にはポインタになってしまっているということです。

実態はポインタ

func関数の実態は次のように宣言した場合と等価になります。

void func(int *a)
{
  printf("%zu\n", sizeof(a));
}

仮引数が配列のような振りをしているのはあくまでも見た目だけで、その性質は完全にポインタです。
sizeof演算子のオペランドにしたときもポインタの特性を示していますし、アドレス演算子の場合もやはりそうです。

さらにいえば、仮引数のaにfunc関数の中で代入することができてしまいます。
こんな風にです。

void func(int a[3])
{
  a = NULL;
}

もしaが本当に配列であればこんな代入は絶対にできません。

なぜこんなことが起きるかというと、ここでもやはり配列から先頭要素への暗黙の変換がかかわっています。
func関数の仮引数aは実引数として渡した配列の先頭要素へのポインタなんです。

ポインタなのに配列として仮引数を宣言できてしまうのは本当に罠です。
実際がどうであれ、プログラマーの気持ちとしては配列を関数に渡している場合が多いのでこんな表現を許しているのかもしれませんね。

前回もちょっと強めの表現で書きましたが、配列とポインタを同一視してしまう方が結構いらっしゃるようですので本当に注意してください。
先日10年近くプログラマーとして活動されている方とお話しする機会がありましたが、その方も混同されていました。

その方の場合は関数が受け取る仮引数がポインタ型だということはご存じだったのですが、それでも何らかの事情でsizeof演算子のオペランドになった場合は配列型として振る舞うと思っていらっしゃいました。
そんなことは決してありません。

要素数の省略

関数の仮引数を配列として宣言しても実際にはポインタになってしまうということは、配列の要素を書いても意味が無いということです。
もちろん、実引数として渡す配列が何要素持つ必要があるかというメモが目的なら要素数を書いてもいいとは思いますが、異なる要素数の実引数を渡してもコンパイラはエラーも警告も出しません。

要素数を書いても意味が無いのであれば、要素数を省略してしまってもかまいません。
こんな感じにです。

void func(int a[])

たとえば引数として受け渡しする配列が文字列だったとしましょう。

#include <stdio.h>

void func(char s[])
{
  ...
}

int main(void)
{
  func("hello");
  return 0;
}

以前の記事で書いたように、文字列はchar型の配列で末尾にナル文字が格納されています。
何要素あるかはナル文字を見つけることでわかりますので、仮引数に要素数を指定する必要はありません。

多くの場合、仮引数に配列を書き場合には要素数は省略することが多いようです。
でも、もっと多いのは、誤解を招くような書き方を避けるために、仮引数は配列ではなくポインタとして宣言することの方が多いと思いますし、私もその方がいいと思います。

実引数の要素数を関数に伝える

Cでは、仮引数として受け取った配列(実際はポインタ)を使ってその配列の要素数を取得することはできません。
でも要素数が必要になることは普通にありますよね?
そういう場合は、ほかの方法を使って要素数を関数に教えてあげる必要があります。

一番わかりやすいのは別の引数として要素数を与える方法です。
たとえばこんな感じです。

void func(int a[], size_t n)

または

void func(int *a, size_t n)

上の例では、aは何要素かわからない配列を受け取りますが、その要素数をnとして渡しています。
実際にaの要素数がn個かどうかはコンパイル時にも実行時にも判断することはできませんので、整合性が取れていることはプログラマーが保証しなければなりません。
Cではこの方法が一番よく使われていると思います。

ほかの方法としては次のような書き方もあります。

void func(size_t n, int a[n])

この例では、第1引数nが第2引数aの要素数であることを明示しています。
ただ、こういう書き方ができるというだけで、やっぱりコンパイル時にも実行時にも何のチェックもしてくれないんですけどね。

この書き方は可変長引数というんですけど、可変長引数については別の機会に詳しく解説しようと思います。

もう少しマシな方法に次のような書き方もあります。

void func(int a[static 3])

ちょっと変わった書き方ですね。
このように要素数の前にstaticを付ける書き方は関数の仮引数の場合にだけできます。
この書き方をした場合、配列aは少なくとも3要素以上あることが保証されます。

保証されますと書きましたが、保証するのはあくまでもプログラマーのあなたです。
もし3要素に満たない配列を渡してしまうと未定義の動作になります。
でも、コメントで書くよりは意味が明確になりますよね。

ちなみにこの書き方を知らないCプログラマーは大勢いらっしゃいます。
Cの場合、プログラマーは言語のことをちゃんと理解していることが前提だと思うんですけど、現実を見ると必ずしもそうではありません。
とはいえ、自分が知らない書き方が出てくれば、自分で調べるとかせめて詳しい方に聞くとかされると思うので、遠慮無く書いていいと思いますよ。

ところで、指定した要素に満たない場合は未定義の動作になると先ほど書きました。
実際にGCCで試してみると興味深い結果になります。
どうなるかというと、エラーも警告も出さずに単に無視されるんです。
未定義の動作なのでそれでもコンパイラのバグじゃないんですよ。

それでは今回の解説は以上となります。
ちょっと難しかったかもしれませんが大丈夫でしょうか?
配列とポインタは別の概念なので絶対に混同しないでくださいね。