わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

仮引数の配列変数は,ポインタなのか配列なのか

…いろいろおかしい.まず「正確に10×20のint配列に限られる」というのは間違いで,このように書いても,関数内のmy_arrayは,ポインタ変数になります*1.
*1:関数プロトタイプ内のmy_arrayは,配列変数と見なしても,仕様上,問題ありません.とはいえ宣言なので,変数の実体は作られませんが.

1990年代のCプログラミング - わさっき

に対して,Einherjar様とコメントを交わしました.
さらにEinherjar様も,プロトタイプ宣言中の配列の引数 - ProsBloomで,「関数定義の仮引数として配列を書いても,ポインタに置き換わる」例を挙げられています.その主張については,私も同意します.
そこで問題を整理すると,次の2点になります.

  • 「関数の仮引数として配列形式で書いても,呼び出し時に,ポインタとして扱われる」ことについて,その根拠はあるか?
  • 関数の仮引数として配列形式で書いたとき,関数宣言の時点では,それは配列なのかポインタなのか? それを裏付ける根拠はあるか?

注釈を二つ.まず,配列形式には,要素数を定数として指定する通常の意味の配列だけでなく,要素数を記載しない,不完全型の配列も該当します.それから,関数宣言には,関数プロトタイプだけでなく,関数定義の最初の箇所も含めます*1
以下,この種の問題に私が出合ったら常に参照している,『C言語によるプログラミング―スーパーリファレンス編*2を通じて,この問題の答えを探っていきます.
まず,上の問題に関係しそうな箇所を引用します.

関数の引数に不完全型が記述される場合もあります.次の例は,配列xの寸法nが与えられた場合,その値を利用して配列データの総和を求めるプログラムです.

double SumUp( int n, double x[] )
{
  (略)
}

このようにすることで,大きさの不明な配列の総和を求めることができます.これを呼び出すプログラムを次に示します.zはその寸法が初期化の部分から分かるのでオブジェクト型です.

(略)
  double z[] = { 2.3, 4.6, 4.7, 8.5, 2.3 };
(略)
  sm = SumUp (sizeof z / sizeof z[0], z );
(略)

(pp.67-68)

ちなみに不完全型のオブジェクトに対してsizeof演算子は適用できませんので,次のプログラムはエラーとなります.

double SumUp_1( double x[] )
{
  int k;
  int n=sizeof x/sizeof x[0];
  double s=0.0;

  for( k=0; k<n; k++ ) s += x[k];
  return s;
}

(p.68)

引用ではありませんが,気になった点を挙げておきます.派生型(9.5節,pp.71-89)において,関数型への派生で,戻り値の型に基づき関数型を設定していますが,仮引数の型に配列を使用している例はありませんでした.ポインタ型を使用している例が一つだけ,ライブラリ関数signalの型を説明する箇所(p.87)にあります.しかしこれは,仮引数に関数を指定するために「関数のポインタ」を使用している,というものです.結局,通常よく書かれる配列やポインタを仮引数として,関数型を派生させている例が見当たりませんでした.

  • 関数名: 関数に付けられた名前.関数型を持つ
  • 関数原型名(プロトタイプ宣言): 関数原型(プロトタイプ)に付けられた名前.関数型を持つ

(p.103,表「宣言によって示せる識別子」より抜粋)

double型の配列(要素は10個)を引数として持ち,関数(double型の引数を2つ持ち,double型を返す関数)へのポインタを返す関数

double (*funcp(double x[10]))(double, double)
{

}

(略)
この関数は,次のように呼び出します.

double (*f)(double, double); /* double型の引数を2つとり,double型を返す関数へポインタ */

double t[10];

f = funcp( t ); /* 関数funcpを実行し,fに関数へのポインタをセットする */

(pp.353-354)

仮引数に配列型が指定されていても,実際には,配列のための領域は確保されません.配列型の仮引数は,その先頭要素を指すポインタ型に変換されます.
たとえば,次の関数は,不完全型の配列と加算すべきデータの個数を受け取り,その個数分だけ配列データの内容を集計する関数です.

double sumup( double x[ ], int n )
{
  (略)
}

これは,たとえば,次のようにして,呼び出します.

double x_data[ 1000 ];

s = sumup( x_data, 1000 );

しかし,この呼び出しによって,x_dataの全データが関数sumupの仮引数xにコピーされるわけではありません.配列x_dataは関数sumupの呼び出しに先立ち,x_dataの先頭要素を指すポインタに変換されます.したがって,double型を指すポインタだけが仮引数xに渡されます.なお,仮引数が配列の場合,その実体はポインタなのでその値を変えることができます(たとえば,この例で用いられているsumupでは仮引数xにdoubleを指すポインタの値を代入することができます)が,プログラムの安全性の観点から絶対に行わない方がよいでしょう.
(pp.357-358)

なお,以下の表に実引数とそれに対応する仮引数の例を示します.

  • 実引数
    • 仮引数
  • double xa[10]
    • double y[]
    • double y[10]
    • double *y
  • double xb[2][3]
    • double y[2][3]
    • double y[][3]
    • double (*y)[3]
  • "string"
    • char y[]
    • char *y

(p.358,表は箇条書きにした)

以上から,考察します.

  • 冒頭に挙げた問題,すなわち『「関数の仮引数として配列形式で書いても,呼び出し時に,ポインタとして扱われる」ことについて,その根拠はあるか?』については,明快な根拠が見つかりました.上記のpp.357-358のところです.
  • p.68に『仮引数が不完全型の配列のとき,関数内部で「sizeof その仮引数」を書くとエラー』という趣旨のことが書かれていますが,これはpp.357-358の記述に矛盾しますし,配列か否かを見極めるにはsizeof - わさっきで検証コードを挙げています.ここは「エラー」ではなく「仮引数であるポインタのサイズを求める」というのが妥当なのでしょう.(ともあれ,関数SumUp_1は期待通りに動かないのですが.)
  • p.103の例と,pp.357-358から,関数プロトタイプであっても,仮引数が配列のときこれをポインタに変換すべきだという主張に説得力があります.というのも,もし配列のままだと,関数プロトタイプでのその関数は配列を含む関数型で表現されることになり,関数定義における関数型(こちらでは仮引数の配列はポインタに置き換えられる)と型が合わないことになる*3からです.
  • pp.353-354の関数funcpの例とその直前の説明文,そしてp.358の対応例から,仮引数に要素数を指定した配列変数を書いた場合,その要素数と一致する配列を実引数とすることが要請されているようです.しかし,pp.357-358にあるように,仮引数は関数内ではポインタ型ですし,実引数に配列変数を書いても,関数呼び出しの際にはポインタ値になります.
  • 仮引数と実引数で配列の要素数のみが異なる場合にはエラー,といった記述はありませんでした.しかしこれも,pp.357-358の説明から,仮引数はポインタ,実引数もポインタ値なので,そのポインタ型が整合していれば,コンパイルエラーや実行時エラーは発生せずに,代入されると解釈すべきでしょう.しかしこれも,『プログラムの安全性の観点から絶対に行わない方がよいでしょう』ね.

さて『関数の仮引数として配列形式で書いたとき,関数宣言の時点では,それは配列なのかポインタなのか? それを裏付ける根拠はあるか?』についてですが,明確な根拠はないものの,上の箇条書きの3番目に書いたように,宣言により得られる関数型に着目すると,ポインタに変換されるというのが妥当そうです.
ですので,7月3日の脚注は,「関数プロトタイプ内のmy_arrayについては,この変数の実体は作られません.しかしmy_functionの型(関数型)を定めるという観点では,これもポインタ型になると考えるべきでしょう.」に訂正します.

*1:それ以外にないと思いますが.

*2:1年後期のプログラミング講義を受け持った初めての年から,授業準備でよく参照している本です.そして昨年,学科の数名の教員で1〜2年生向け教科書を選定する際に,私も強く押しまして,最終的に選んだ1冊です.

*3:関数の型の適合については,pp.96-97で言及されています.しかしここでの議論に参考になる情報が見当たらなかったため,引用はしません.