たのしい工学

プログラミングを学んで、モノをつくりたいひと、効率的に仕事をしたい人のための硬派なブログになりました

【独学するコンピュータサイエンス】関数と式

   

関数と式

関数を式として使用するには関数名の記述に続き、C 言語で優先順位が最も高い演算子の一つである「関数呼び出し演算子 ( ) 」を記述し、その演算子の括弧の中に実引数を複数あればコンマで区切って記述します。関数呼び出し演算子は関数を呼び出し、実引数を関数に引き渡し、その関数の戻り値が式の値となります。

int func( int i )
{
return ( i * 2 );
}
この関数を使用した式を次のように記述します。
int a = func( 10 );
func は関数名には違いないのですが正確には関数型を持つ式であり、これを関数指示子 (function designator) と呼びます。そして後置演算子である関数呼び出し演算子を使用して引数を 10 と指定しています。つまり右辺の式は func と ( ) 、そして 10 に分解出来ます。 func も 10 も一つの式、項です。この二つの項を関数呼び出し演算子 ( ) によって演算する事で右辺の式の値が決まります。決して func( 10 ) が一つの項なのではありません。

関数とポインター

関数指示子はある例外を除き「ある型のオブジェクトを返す関数へのポインター」の式に自動変換されます。

int func( int a )
{
return ( a + 1 );
}
この関数を使用した式を次の様に記述します。

int z = func( 9 );
func( 9 ) に於ける func は関数指示子であり項です。従って func そのものがポインターに変換されます。
関数呼び出し演算子はその対象となる項が関数へのポインターでなければなりません。しかし関数指示子はポインターに自動変換されるのでそれを意識する必要はありません。
自動変換されないのは二例だけ。一つはアドレス演算子 & の対象項となった場合。もう一つは sizeof 演算子の対象項となった場合です。しかし関数型を持つ式に対して sizeof 演算子は使用出来ないという制約があるので、結局 sizeof 演算子は関数指示子に対して使用出来ません。アドレス演算子については後述します。

関数へのポインターは別の関数へのポインターに型変換出来ます。しかし変換前と変換後の引数の型や数、並びも含めた関数の型が適合していない場合、変換後の関数へのポインターを使用した時の動作は保障されておらず未定義です。

関数型オブジェクト

関数型も型であるからにはそのオブジェクトが作成可能です。
ここで関数型の変数の宣言の仕方を考えます。次のプロトタイプ宣言の関数を例にします。

int func( int a, char b );
関数型は「ある型を返す関数」が型ですから、この場合「 int を返す関数」がそれですね。作成する変数の名前は f としましょう。
int f;
としてしまうと通常の int 型変数に過ぎません。
関数型は型の他に引数によって特徴付けられます。ですから引数も記述する必要があります。
int f( int a, char b );
プロトタイプ宣言そのものになってしまいました。ではこれで f は関数指示子なのでしょうか。関数指示子は式の中ではポインターに自動変換されます。つまりポインターでなければなりません。これを加味したつもりで次の様に記述するのは誤りです。

int * f( int a, char b ); /* 誤り */
プロトタイプ宣言としてみれば分かり易いです。読み解くと「 f は int * 型を返す関数で、int 型の第一引数と char 型の第二引数があります」といった感じですね。 func は int 型で f は int * 型となり型が一致しません。
型を一致させる為に優先順位を変更する括弧を用いて次の様にします。

int ( * f )( int a, char b );
f がポインターである事を示す為にポインター宣言子 * を変数名の前に付けるのは同じですが、 * f を括弧で括ります。これにより f は int 型を返す関数へのポインターとなります。配列へのポインター char (*s)[ 5 ]; 等と同じ様に考えて宣言を読み解いてください。
仮引数の扱いはプロトタイプ宣言と同じく配列を指定しても一次元目がポインター扱いされます。
このままでは f が初期化されていませんから変数として使えません。ポインターですからアドレスを代入します。これまでのポインターと同じく好き勝手なアドレスを代入してはいけませんし意味がありません。ここでは func 関数のアドレスを使います。
宣言と同時に初期化を記述した場合は次の様になります。

int ( * f )( int a, char b ) = func;
func は関数指示子ですからそれ自体が式です。関数指示子はポインターに自動変換されるのでこの様な記述になります。

func と f を使った次の三行は同じ意味ですし同じ動作をします。

func( 1, 2 ); /* A /
f( 1, 2 ); /
B /
(
f)( 1, 2 ); /* C /
A は通常の関数呼び出し演算です。( ) は演算子である事をお忘れなく。関数呼び出し演算子 ( ) はその対象となる項が関数へのポインターでなければなりません。 func は関数指示子ですから関数へのポインターに自動変換されます。従って演算は正しく行われ func 関数に対して 1 と 2 を引数として渡します。
B について。 f は関数へのポインターです。 f のアドレスは func と同じであり且つ引数の型と個数そしてその並びも同じですから A と同じ動作になります。
C です。間接参照演算子 * の特例に「対象項が関数型を指し示している場合その演算結果は関数指示子になる」という規定があります。要するに「関数へのポインターに間接参照演算子 * を用いると、その結果は関数指示子になる」と云う事です。 B の f は関数へのポインターですが、 C の (
f) は関数指示子になっています。即ち func と同じ扱いです。*f( 1, 2 ) では間接参照演算子 * より関数呼び出し演算子 ( )の方が優先順位が高い為に意味が変わってしまうので、 *f 前後にある優先順位を変更する括弧は必須です。
関数指示子は関数へのポインターになりますから結局 A B C の三つは同じ結果となるのです。

間接参照演算子の特例をよくよく考えると、関数へのポインターに対して間接参照演算子を用いてもその結果は用いない時と何も変わらないのが分かるでしょう。 ( **f )( 1, 2 ); となっている場合を考えます。 **f は ( *f ) ですね。 f は関数へのポインターですから *f の結果は関数指示子です。ところが関数指示子は関数へのポインターになるので *f も f も意味は同じです。そこで **f を考えると *( *f ) の括弧内 *f は f と同じです。従って *( *f ) は *( f ) となり、f です。 *f は f と同じですから結局 **f は f と同じになってしまいます。従って次の記述も有効です。

(******f)( 1, 2 ); /* C' /
添字演算子を用いた a[ 5 ] を 5[ a ] と記述するのと同様冗談の類と思ってください。実際に使用する事はありません。
関数へのポインター変数を使用した関数呼び出しである事を明示する為に (
f)( 1, 2 ) と表記するのも良いでしょう。勿論 f( 1, 2 ) でも構いません。

関数指示子に対して間接参照演算子を使えるのでアドレス演算子 & も使えます。関数指示子に対してアドレス演算子を用いた結果は関数へのポインターになります。

(&func)( 1, 2 ); /* A' */
func は関数指示子ですから通常は関数へのポインターになります。ところがアドレス演算子の対象項となった場合はこの自動変換がありませんので関数指示子のままです。そして関数指示子に対してアドレス演算子を用いるとその結果は関数へのポインターとなります。従って関数指示子にアドレス演算子を用いても無意味です。
気を付けて欲しいのは関数へのポインターに対してアドレス演算子は使用出来ない点です。つまり次の記述は不可です。

(&f)( 1, 2 ); /* B': f は関数へのポインターなので不可 */

今回はここまでです!
ではでは

 - コンピュータサイエンス