C言語編 第10章 関数プロトタイプ

先頭へ戻る

この章の概要

この章の概要です。


main関数の記述位置

前章のサンプルプログラムでは、自作の関数の定義をプログラムの先頭近くに記述し、 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. 引数が可変個のときには、固定の部分は上記に従い、可変個部分は ... と記述する

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 に対応。

≪更に古い更新履歴を展開する≫



前の章へ(第9章 関数)

次の章へ(第11章 処理の流れを分岐させる)

C言語編のトップページへ

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー