[前へ]   [目次へ]   [次へ]

関数(2)(C/C++)

C/C++言語は自分で独自の関数をどんどん追加することができます。
今回はその関数の定義方法についてです。

前回、特に大した解説もなく関数 f を定義して使用しました。
今回はその部分についての解説です。
<  1>
<  2>
<  3>
int f(int x){
   
return 2*x;
↑は前回の関数定義部分です。

C/C++の関数の定義構文は以下のようになります。

戻り値型 関数名(引数リスト){
   関数として定義するプログラム
}


上記の場合は、戻り値型 int 、関数名 f 、引数リスト int x 、
定義したプログラムは return 2*x; にあたります。

また、以下のようにすると宣言のみで定義はしません。
この宣言を「プロトタイプ宣言」と呼びます。

戻り値型 関数名(引数リスト);

宣言のみされた関数は以降、呼び出す記述をすることはできますが、
リンク段階で定義が見つからなかった場合、エラーになります。
事前に全ての関数を宣言しておくことにより、関数の定義順序を考慮しなくても、
全ての関数を呼び出せるようにできます。
規模が大きくなってくるとイロイロメリットがあるので、積極的に使っていくといいでしょう。

宣言のみされている関数を定義するためには、
戻り値型と関数名、引数リスト全てを一致させた関数を定義します。
全て一致した関数が定義されると自動的に関連付けられます。

C言語では同じ関数名で複数定義することはできませんが、
C++言語では同じ関数名でも引数リストが異なる場合は複数定義することができます。
この場合、コンパイラは実引数(後述)の型とより近い引数リストの関数を呼び出すようになります。

なお、関数の定義、宣言は他の関数定義内ではできません。

ちなみに、関数にも変数でいうところの「装飾」があるのですが、
これまでの解説内容だけでは説明しようがないものだらけなので、
現在のところは省略します。


さて、詳細を解説します。
「戻り値型」はこの関数を呼び出した時(前回の y=f(2) の部分)、
関数呼び出し演算子の処理後の型になります。

「関数名」は変数名同様、今から定義する関数の名前になります。
命名規則は変数名と同様のルールが適用されます。

「引数リスト」はこの関数を呼び出す時に要求する引数の型と数と並びを指定します。
原則として、関数はこの「引数リスト」と関数呼び出し演算子の()内の引数が一致できなければ、
エラーが発生してコンパイルが通らないようになっています。

この時、関数宣言/定義の引数のことを特に「仮引数」(かりひきすう)、
関数呼び出し演算子の()内の引数のことを特に「実引数」(じつひきすう)と呼ぶことがあります。
どちらかを区別する必要がある場合はこのように呼ぶ場合がありますが、一般的には「引数」で通ります。

引数リストは通常の変数定義と同じ書式を用いますが、
, での型名の省略はできません。
複数の引数を要求する場合は , 区切りで必要な数だけ変数定義を行います。
また、配列を型名にすると特別な扱いを受けます(後述)。
さらに、リストの最後が ... で終わると「以下略」というこれも特別扱いになります。
これについては現時点で解説するのは難しいため、今は解説しません。

なお、区切り地点(戻り値型と関数名の区切りなど)を判断するのに必要なスペース、タブ以外は無視されます。
また、引数を取らない(リストなし)の場合は void と書きます。(C++では空にしても同様)

例1
A   B   C   C2
int f ( int x  )

Aが戻り値型、Bが関数名、Cが第一引数の型名、C2が第一引数の変数名です。

例2
A   B    C      C2   D
int f2 ( double x  , y )

Aが戻り値型、Bが関数名、Cが第一引数の型名、C2が第一引数の変数名、Dが第二引数の変数名です。
変数定義の感覚では x y ともに double 型になるように思いますが、
上述の通り、引数リスト内では型名省略はできないため、 y は double 型になりません。
両方とも double 型にするためには、次の例3のようにしなければいけません。

例3
A   B    C      C2   D      D2
int f2 ( double x  , double y  )

Aが戻り値型、Bが関数名、Cが第一引数の型名、C2が第一引数の変数名、
Dが第二引数の型名、D2が第二引数の変数名です。


配列を型名にした場合、暗黙にポインタ型への変換が行われます。

例4-1
A   B   C   C2 C3
int f ( int x  [10] )

Aが戻り値型、Bが関数名、Cが第一引数の型名、C2が第一引数の変数名、C3が第一引数の要素数です。
C3で要素数を指定したため、配列定義の形なのですが、これは以下の定義に勝手に置き換えられて解釈されます。

例4-2
A   B   C    C2 
int f ( int* x  )

Aが戻り値型、Bが関数名、Cが第一引数の型名、C2が第一引数の変数名です。
これは配列名を式中で使った場合に行われる変換と同じものです。
そのため、配列名をそのまま渡すことができます。
重要な点としては、例4-1のような定義を使うとあたかも
「配列を丸ごとコピーして渡しているように見える」 のですが、
実際には 「配列のコピーなんて作られていない」 という点です。
引数に受け取った「配列に見えるもの」を編集すると呼び出し元に影響が及びます。
なんでそのようなことが起こるかは次節以降で解説します。

例4の変換はあくまで「配列名を直接渡せる」ように変化するので、
二次元配列のような場合はややこしいことになります。

例5-1
A   B   C   C2 C3   C4
int f ( int x  [10] [20] )

Aが戻り値型、Bが関数名、Cが第一引数の型名、C2が第一引数の変数名、
C3が第一引数の要素数その1、C4が第一引数の要素数その2です。
C3、C4で要素数を指定したため、二次元配列定義の形なのですが、これは以下のように変換されます。

例5-2
A   B   C     C2 C3   C4
int f ( int ( *  x  ) [20] )

Aが戻り値型、Bが関数名、C、C2、C4が第一引数の型名、C3が第一引数の変数名です。
なんかややこしい表記になってますが、これの型は普通に示しても int(*)[20] というややこしい型です。
これは「配列のポインタ」という代物なのですが、用途が非常にレアなモノです(笑)
これを使おうということはあまりないと思いますが、配列を渡そうと例5-1のような記述をして、
コンパイラが int(*)[20] に変換できませんなどというエラーを提示する場合があります。
そういう時はこの辺の暗黙変換に注意してみると、原因が分かることがあります。
(注:コンパイラによっては int[][20] とする場合がありますが、ここでは二つの表現は等価となります)

例6
A   B   C
int f ( void )

Aが戻り値型、Bが関数名、Cが引数リストです。
ここに void を書くと、引数なしと扱われます。
呼び出し時は f() などのように () 内を空にして呼び出します。
注:スクリプト言語でよくある「関数名は( )なしでも呼び出しとする」という規定はC/C++にはないので、( )は省略できません。

さてさて、それでは実行可能な例を示しましょう。
<  1>
<  2>
<  3>
<  4>
<  5>
<  6>
<  7>
<  8>
<  9>
< 10>
< 11>
< 12>
< 13>
< 14>
< 15>
< 16>
< 17>
< 18>
< 19>
< 20>
< 21>
#include <stdio.h>

int f(int x){//(1)
   
return 2*x;
}
double g(double x){//(2)
   
return 2.0*x;
}

int main(void){
   
int y;
   
double y2;
   y=f(2);
//(3)
   
y2=g(2.5);//(4)
   
printf("%d\n%f\n",y,y2);//(5)
   
y2=f(2.5);//(6)
   
printf("%f\n",y2);//(7)
   //終了待ち
   
getchar();
   
return 0;
実行結果:
4
5.000000
4.000000

(1)3〜5行目は戻り値型を int 、(2)6〜8行目は戻り値型を double として関数定義しています。
どちらも2xを返す関数として定義され、引数と戻り値の型が異なるのみです。

(3)13行目は前回と同様の呼び出しです。
この式を追いかけて見ます。
A B C D E D'
y = f ( 2 )


通し記号種別
Aint不定(未初期化)変数(y)
B演算子(=)(2項、算術、優先2、結合←) 
Cint(*)(int)0x0040100A関数(f)
D演算子( () )(その他、優先16、結合→)
Econst int2定数
D'演算子Dの終点
優先順位から、演算子D(関数呼び出し、左辺 関数C、引数 定数E)から処理されるのですが、
今回はこの関数呼び出し演算子の行う処理を追いかけてみます。

演算子Dの左辺の関数は先ほど(3〜5行)定義した関数 f です。
(1-1)関数呼び出しが開始されると、まず呼び出す関数の仮引数が関数のローカル変数として作成されます。
仮引数は関数ブロック所属のローカル変数となり、
スコープおよび寿命は3〜5行の範囲です。

(1-2)次に、実引数が今作成した仮引数に順次代入されます。
代入される実引数と仮引数は左から同じ順番です。
原則、仮引数の数と実引数の数は一致しなければなりません。(一致しない場合はエラーまたは警告が出る)
代入を行うので、必要ならここで暗黙の型変換代入時バージョンが作動します。

(1-3)その後、プログラムの実行座標は呼び出した関数の開始位置に移動されます。
この場合は、4行目の位置が次の実行座標になります。

(1-4)実行座標を移動すると、関数呼び出し演算子の前半処理は完了し、
その後は移動先のプログラムが順次実行されていきます。

(2-1)というわけで、次に実行されるのは return 2*x になります。
初解説の return は実行されると関数の処理を終了して、
呼び出し元の関数呼び出し演算子の後半処理を行います。

原則として、 return には値をひとつ与えなければいけません。
この値はこの後戻り値に使われます。
ただし関数の戻り値型が void である場合、 return には値をつけることはできません。

関数 f の戻り値型は int なので、 return には値が必要です。
値であれば何でもいいので定数でも一時変数でも変数でも式でもOKです。
今回は式 2*x が与えられています。

(2-2) x は関数呼び出し演算子の前半処理によって、
呼び出し時の実引数(2)が代入されているので、 x の値は2です。
よって、 2*2 で4になります。
この計算結果が return に与えられます。

(2-3)さて、 return が実行されると実行座標は
その関数の関数呼び出し演算子を実行している位置に戻されます。
そして、関数呼び出し演算子の後半処理が実行されます。
さらに後半処理も追いかけてみます。

(3-1)後半処理が開始されると、関数の戻り値型で一時変数が作成されます。
ただし、呼び出した関数の戻り値型が void である場合は、作成されません。
この場合は関数 f の戻り値型 int で一時変数が作成されます。

(3-2)一時変数の作成が行われた場合は、
続いて呼び出した関数で実行した return に与えられた値(2-2のヤツ)が
その一時変数に代入されます。
ここでも必要なら暗黙の型変換代入時バージョンが作動します。
この場合は4が一時変数に代入されることになります。

(3-3)最後に return した関数 f のローカル変数が全て破棄され、消滅します。

これで、関数呼び出し演算子の実行が完了です。
↓実行後はこうなってます
A B C
y = vtemp1


通し記号種別
Aint不定(未初期化)変数(y)
B演算子(=)(2項、算術、優先2、結合←) 
Cint4一時変数(3-1で作成したもの)
と、いうわけで y は4になります。



(4)14行目は double 型バージョンの関数呼び出しです。
この式も追いかけて見ます。
A  B C D E   D'
y2 = g ( 2.5 )


通し記号種別
Adouble不定(未初期化)変数(y2)
B演算子(=)(2項、算術、優先2、結合←) 
Cdouble(*)(double)0x00401005関数(g)
D演算子( () )(その他、優先16、結合→)
Econst double2.5定数
D'演算子Dの終点
今度の関数は戻り値型、引数型ともに double です。
定数も、このように小数点を含んでいる場合は型が const double として扱われます。

優先順位から、関数呼び出しが行われます。
先ほどと同様の手順で
(1)関数 g の仮引数 double x がローカル変数として作成され、実引数の 2.5 が代入されます。
(2)実行座標が関数 g の起点、7行目にセットされます。
(3) return 2.0*x の式、 2.0*x の演算が行われます。
A   B C
2.0 * x

通し記号種別
Aconst double2.0定数
B演算子(*)(2項、算術、優先13、結合→) 
Cdouble2.5変数(x)
同じ型なので、そのまま掛け算され、 5.0 になります。

(4)(3)の結果が return され、関数 g のローカル変数 x は破棄されます。
そして、 y2=g(2.5) の続きに戻ります。
関数呼び出し演算子の実行が完了したので、残りは以下のようになります。

A  B C
y2 = vtemp1


通し記号種別
Adouble不定(未初期化)変数(y2)
B演算子(=)(2項、算術、優先2、結合←) 
Cdouble5.0一時変数
同じ型どうしなので、そのまま代入されます。



(5)15行は、先の二行の関数呼び出しで得られた呼び出し結果を画面に表示しています。
printf 関数で double 型の変数を表示する場合は %f 変換指定を使います。
ここで間違えて %d としてしまうと訳のわからない値が表示されるので注意してください。


(6)16行は、 double 型の変数と引数を使って int 型仕様の関数 f を呼び出しています。
この式も追いかけて見ます。
A  B C D E   D'
y2 = f ( 2.5 )


通し記号種別
Adouble5.0変数(y2)
B演算子(=)(2項、算術、優先2、結合←) 
Cint(*)(int)0x0040100A関数(f)
D演算子( () )(その他、優先16、結合→)
Econst double2.5定数
D'演算子Dの終点
先ほどと同様の手順で
(1)関数 f の仮引数 int x がローカル変数として作成され、実引数の 2.5 が代入されます。
が、この時型が異なるため、暗黙の型変換代入時バージョンが作動し、
実引数の double 型の値 2.5 は int 型に変換されます。
この時、小数点以下が捨てられてしまうので値は 2 になります。

(2)実行座標が関数 f の起点、4行目にセットされます。
(3) return 2*x の式、 2*x の演算が行われます。
A B C
2 * x

通し記号種別
Aconst int2定数
B演算子(*)(2項、算術、優先13、結合→) 
Cint2変数(x)
同じ型なので、そのまま掛け算され、 4 になります。

(4)(3)の結果が return され、関数 f のローカル変数 x は破棄されます。
そして、 y2=f(2.5) の続きに戻ります。
関数呼び出し演算子の実行が完了したので、残りは以下のようになります。

A  B C
y2 = vtemp1


通し記号種別
Adouble5.0変数(y2)
B演算子(=)(2項、算術、優先2、結合←) 
Cint4一時変数
最後に、代入時の型が異なるので、もう一度暗黙変換代入時バージョンが作動します。
一時変数Cは int から double に変換され、値は 4.0 として代入されます。

この結果、最後(7)17行目 の出力では4.000000と出ています。
このように引数型や戻り値型によって結果が異なる場合があるので、
値の精度が失われたりしないよう、注意する必要があります。


次回からは、関数については一時休止して、「宣言、定義の正体」の予定です。
これは「関数」の他の要素を含め、この先の要素の理解には必須だと私は考えています。
いわゆる「中ボス」みたいなモノなので、頑張って臨んでください。

[前へ]   [目次へ]   [次へ]

プログラミング講座 総合目次

最終更新 2008/10/17