この章の概要です。
前章のサンプルプログラムでは、自作の関数の定義をプログラムの先頭近くに記述し、 main関数を下の方に記述していました。 これは、このような順番で記述しないと、意図どおりにコンパイルができないからです。 それはなぜなのか考察してみましょう。 実際に、main関数を上の方にもってくると、次のようになります。
#include <stdio.h> int main(void) { double base, height, area; base = 3.0; height = 5.0; area = calcAreaOfTriangle( base, height ); printf( "底辺:%f 高さ:%f 面積:%f\n", base, height, area ); base = 7.0; height = 4.0; area = calcAreaOfTriangle( base, height ); printf( "底辺:%f 高さ:%f 面積:%f\n", base, height, area ); base = 4.4; height = 3.6; area = calcAreaOfTriangle( base, height ); printf( "底辺:%f 高さ:%f 面積:%f\n", base, height, area ); return 0; } /* 三角形の面積を求める。 引数 base: 底辺の長さ。 height: 高さ。 戻り値 面積。 */ double calcAreaOfTriangle(double base, double height) { return ( base * height / 2 ); }
このプログラムを、VisualStudio 2017 でビルドしてみると、次のようなエラーメッセージが表示されます。
warning C4013: 関数 'calcAreaOfTriangle' は定義されていません。int 型の値を返す外部関数と見なします。 error C2371: 'calcAreaOfTriangle' : 再定義されています。異なる基本型です。
コンパイラによって、エラーや警告の有無や、報告される数は違うことがありますが、 いずれにしても、意図通りの動作にはなりません。
何がエラーで、何が警告になるかは、コンパイラの裁量次第であり、C言語の規格上は明確には定められていません。 ですから、コンパイラが何も文句を付けないからといって、そのプログラムに問題が潜んでいないという保証はありません。 なお、警告だけならば実行することができるコンパイラが多いでしょうが、警告も安易に無視してはいけません。
ここで問題なのは、「関数 'calcAreaOfTriangle' は定義されていません」の部分です。 この警告は 10行目で出ているので、main関数から calcAreaOfTriangle関数を呼び出している部分ですが、 要するに、この位置からは calcAreaOfTriangle関数の定義が見つけられないのです。 なぜ見つけられないかというと、「10行目よりも手前に calcAreaOfTriangle が登場していないから」です。
こういう場合、コンパイラは勝手に、関数の定義の存在を想定します。 先程のエラーメッセージで、「int 型の値を返す外部関数と見なします。」とあるのがそれです。 本当は calcAreaOfTriangle関数は、引数が double型2つで、戻り値も double型のはずなのに、 int型の値を返す関数とみなされてしまっては、当然、正しく動作しません。
これ以外にエラーや警告が出ていても、それは副次的に発生したものでしょう。 問題の根本は、calcAreaOfTriangle関数の定義が見つからず、勝手な解釈を行ってしまう点にあります。
関数定義に対して、宣言という考え方もあります。 両者の違いは、関数本体を記述しているかどうかです。 例えば、calcAreaOfTriangle関数の宣言は、次のように書きます。
double calcAreaOfTriangle(double base, double height);
{ } で関数の本体部分を記述する代わりに、セミコロンで完結させます。
関数宣言は、どこか別の場所に、関数定義があることを表現しているだけなので、 関数宣言だけでは、その関数を呼び出すことはできません。関数定義が必要です。 とはいえ関数宣言があれば、コンパイルをきちんと通すことができます。
#include <stdio.h> /* 関数宣言 */ double calcAreaOfTriangle(double base, double height); int main(void) { double base, height, area; base = 3.0; height = 5.0; area = calcAreaOfTriangle( base, height ); printf( "底辺:%f 高さ:%f 面積:%f\n", base, height, area ); base = 7.0; height = 4.0; area = calcAreaOfTriangle( base, height ); printf( "底辺:%f 高さ:%f 面積:%f\n", base, height, area ); base = 4.4; height = 3.6; area = calcAreaOfTriangle( base, height ); printf( "底辺:%f 高さ:%f 面積:%f\n", base, height, area ); return 0; } /* 三角形の面積を求める。 引数 base: 底辺の長さ。 height: 高さ。 戻り値 面積。 */ double calcAreaOfTriangle(double base, double height) { return ( base * height / 2 ); }
実行結果:
底辺:3.000000 高さ:5.000000 面積:7.500000 底辺:7.000000 高さ:4.000000 面積:14.000000 底辺:4.400000 高さ:3.600000 面積:7.920000
こうすると、calcAreaOfTriangle関数を呼び出している箇所よりも手前に、関数宣言が現れるので、コンパイルは正しく行われます。 呼び出し位置からは関数宣言しか見えていないはずですが、同じ名前の関数宣言があることで、 きちんと、関数定義を見つけて、それを呼び出すようにコンパイルしてくれます。
普通はこのように、関数定義と同じ名前、仮引数の並び、戻り値の型を指定しますし、これが好ましい記述です。 しかし、次のように書くことも可能ではあります。
double calcAreaOfTriangle();
こうすると、calcAreaOfTriangle という名前の関数が存在していることは示せますが、どんな引数があるのかは示していません。 そのため、実引数を誤って指定しても、コンパイラはチェックすることができません。 例えば、次のような呼び出しを書いても、コンパイルを通してしまいますが、実行すると正しい結果を得られません。
calcAreaOfTriangle(); /* 実引数が足りない */ calcAreaOfTriangle(1.0, 2.0, 3.0); /* 実引数の個数が多い */ calcAreaOfTriangle(1, 2); /* 実引数の型が違う */
このような間違った関数呼び出しが行えてしまうことは、当然、危険ですから、仮引数の記述をきちんとするべきです。 仮引数を空にする記述は、時代遅れの古いやり方です。
関数プロトタイプ(関数原型)は、関数の仮引数の並びを、以下のルールに沿って記述したものです。
1番目は、前の項で取り上げた通り、空の仮引数を避けて void と明記すれば良いというだけのことです。
2番目もこれまで通りです。やはり、空の仮引数を避けていれば問題ありません。 関数宣言の場合には、仮引数を使う本体部分がありませんから、名前は記述してなくても構わないことになっていますが、 どういう引数を渡せばよいのか、という情報になりますから、分かりやすさの面では省略しない方が良いでしょう。 (どうせ、関数定義からコピー&ペーストするので、むしろ消す方が面倒です)
3番目については、第52章で解説するので、今のところは無視しておいて構いません。
関数プロトタイプの記法を使えば、 コンパイラは、仮引数と実引数の対応が一致していないことを検出して、エラーを報告することができるようになります。 安全性を高めるため、常に、関数プロトタイプを使うようにして下さい。
なお、関数プロトタイプは、関数宣言でも関数定義のどちらにでも適用可能な記述法です。 宣言なのか定義なのかは気にせず、常にこの記述方法で書けば問題ありません。
一般的に、関数プロトタイプによる関数宣言を、ソースファイルの先頭付近(#include よりは後)に記述します。 こうしておけば、関数定義をどの位置に書いていても、どこからでも関数宣言が見えるはずなので、 関数を呼び出すコードを安全に書くことができます。
関数プロトタイプによる関数宣言は、関数定義に記述する関数名、仮引数の並び、戻り値の型とまったく同じことを書きます。 もし、関数宣言と関数定義とで、仮引数や戻り値が一致していないと、未定義の動作になってしまいます。 そのため、確実に同じことを書かなければなりません(書くというより、素直にコピー&ペーストするべきです)。
問題① 円周の長さを渡すと、その円の半径を返すような関数を、関数プロトタイプも含めて自作して下さい。 このとき、引数と戻り値の型は double型とします。
問題② 問題①で作成した関数の戻り値を、int型の変数で受け取ろうとするとどうなりますか。
'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。
'2018/2/4 全面的に文章を見直し、修正を行った。
'2017/6/9 C99 で関数プロトタイプが必須だとする記述を削除。
古い書き方は廃止予定になっているが、新しい関数プロトタイプを必須とはしていない。
'2017/3/25 VisualC++ 2017 に対応。
'2015/8/15 VisualC++ 2015 に対応。
![]() |
ツイート | Follow @pplace_ky | |
![]() |
前の章へ(第9章 関数)
次の章へ(第11章 処理の流れを分岐させる)
Programming Place Plus のトップページへ