なぜ関数なのか
ここまでのものはすべて main の中に収まっていました。プログラムが 1 つのことしかしないうちはそれでかまいませんが、そうでなくなると 3 つの問題が一度に押し寄せます。同じコードのかたまりが 3 か所にコピーされ、main は誰も頭の中に収めきれないほど膨らみ、そしてそのコードのかたまりが何のためにあるのか名付ける手段がありません。
関数はこの 3 つすべてを解決します。コードのかたまりに名前、独自のローカル変数、入力のリスト、そして返す 1 つの値を与えてくれます。
square は一度だけ書かれ、自分自身の結果に対する呼び出しも含めて 3 回呼ばれています。あなたはこれまでもずっと関数を呼び出してきました - printf もそうですし、main もそうです。
関数の構造
returnType name(parameterList) {
// 本体
return value;
}
4 つの部分があります:
- 戻り値の型 - 関数が返す値の型。
int、double、char、ポインタ型、あるいは「何もない」を表すvoid。 - 名前 - 変数名と同じ規則に従い、その関数が何をするのか、何を生み出すのかを表すべきです。
- 引数リスト - 入力。それぞれに型があり、カンマで区切ります。
(void)は引数を取らないことを意味します。 - 本体 - 波かっこで囲んだ文の並び。戻り値の型が
voidでない限りreturnで終わります。
pi() は呼び出す側でも空の括弧が必要なことに注目してください - それがあることで、単に名前に触れているのではなく呼び出しになります。そして printBanner(20); はそれ自体が 1 つの文です。使うべき値を生み出さないからです。
引数のない関数には () ではなく (void) と書きましょう。C ではこの 2 つは違う意味を持ち、() のほうがゆるい書き方です - これについてはすぐ後で触れます。
呼び出しと戻り
呼び出しは制御を関数の中へ移し、その本体を実行し、周囲の式の中で呼び出しの位置に置き換わる値を持って戻ってきます。
return は同時に 2 つのことをします。値を提供し、そしてその場で関数を終わらせます。同じ経路上で return より後にあるものは決して実行されません。これこそが larger の早期リターンのスタイルを成り立たせているもので、else は不要です。最後の行に到達した時点で a > b が偽だったことがすでに確定しているからです。
関数は複数の return 文を持てます。ただし 1 回の呼び出しで実行できるのはそのうち 1 つだけです。
void 関数
値ではなくその効果のために存在する関数は、戻り値の型が void になります。表示する、ポインタ越しに何かを更新する、描画する、といったものです。
void 関数の中では、値なしの return; が早期脱出になります。そこで return someValue; と書くのはコンパイルエラーですし、その呼び出しを式の中で使うのも同様です - int x = printTable(7, 5); はコンパイルできません。これは型システムが仕事をしている証拠です。
分割こそが本当の要点
関数は主に繰り返しを避けるためのものではありません。1 つの長い手続きをいくつかの名前付きのステップに変え、main が実装ではなくプログラムの説明のように読めるようにするためのものです。
main はいまや 4 行の出力と 1 つの計算だけになりました。各ヘルパー関数は 1 つのことをし、それが何なのかを表す名前を持ち、単独でテストできます。配列の長さが第 2 引数として一緒に渡されていることに注目してください。配列がいったん関数へ渡されると、そこでは sizeof の技は使えません。配列がポインタに成り下がる(decay する)からです。
処理を関数へ分けるときの 3 つの目安:
- 1 つの関数につき 1 つの仕事。 名前に「〜と〜」が必要になるなら、それはおそらく 2 つの関数です。
- 必要なものを受け取り、生み出したものを返す。 グローバルな状態を読む関数は、理屈を追うのが難しく、単独でのテストは不可能です。
- 一目で見渡せる短さに。 魔法の行数はありませんが、画面に収まらない関数はたいてい、その中にもっと小さな関数を隠し持っています。
関数が関数を呼ぶ
関数は、すでに宣言済みのあらゆる関数を呼び出せます。自分で書いたものも含めてです:
main が report を呼び、それが sumEvens を呼び、それがループの中で isEven を呼びます。呼び出しごとに新しいローカル変数の一式が用意され、呼び出し元のものの上に積まれ、戻るときに捨てられます。
上の定義は依存関係の順 - isEven、sumEvens、report の順 - に並んでいます。C は関数を呼び出す前に宣言されていることを要求するからです。ファイルの下のほうに現れる関数を呼ぶと、コンパイラは暗黙の宣言について文句を言います。一般的な解決策はファイルの先頭付近に置くプロトタイプで、これがあれば定義を好きな順に並べられます。関数が自分自身を呼ぶのも合法で、しかも有用です。それが再帰です。
main も関数である
main は、ランタイムがあなたの代わりに呼び出してくれるごく普通の関数です。その int という戻り値の型はプログラムの終了ステータスです。0 は成功、0 以外は失敗を意味し、シェルスクリプトやビルドツールがそれを読み取ります。
int main(void) {
/* ... */
return 0;
}
C99 以降、main で return なしに末尾を抜けると暗黙的に 0 を返します - これは main にだけ適用される特別な規則です。それ以外の void でない関数が値を返さずに終わった場合、呼び出し元がその結果を使うと未定義動作になります。
もう 1 つの標準的な形は int main(int argc, char *argv[]) で、コマンドライン引数を受け取ります。どちらも正しいですが、古いチュートリアルが何と言おうと void main() は正しくありません。
よくあるミス
- void でない関数で
returnを忘れる。 呼び出し元はゴミの値を読むことになります。gcc -Wallが警告してくれます。 - 本体の中で引数と同じ名前の変数を宣言する。 引数を覆い隠してしまい、入力に対して行ったつもりの代入がどこにも届きません。
- 関数が引数を変更してくれると期待する。 C はすべてを値渡しするので、関数はコピーを受け取ります。
void reset(int x) { x = 0; }は呼び出し側で何も変えません - これが次のページの主題です。 - 呼び出しで括弧を省く。
printBanner;は合法な式で、関数のアドレスを評価して捨てます。コンパイルは通り、何もせず、-Wallが指摘します。 (void)の代わりに()と書く。 合法ですが、古い C の方言では引数チェックが無効になります。
よくある質問
C言語で関数はどう宣言しますか?
戻り値の型、名前、括弧で囲んだ引数リストを書き、続けて波かっこで本体を書きます: int add(int a, int b) { return a + b; }。何も受け取らない関数は (void) を使い、何も返さない関数は戻り値の型を void にします。
C言語の関数で return は何をしますか?
関数をその場で終了させ、呼び出し元に値を返します。値の型は関数が宣言した戻り値の型と一致していなければなりません。void 関数では値なしの return; で早めに抜けることができますし、書かずに閉じ波かっこまで実行させてもかまいません。
C言語の main はなぜ int を返すのですか?
main の戻り値はプログラムの終了ステータスで、OS やシェルスクリプトが読み取れます。return 0; は成功を、0 以外は失敗を意味します。C99 以降、main で return 文なしに末尾へ到達した場合は暗黙的に 0 を返します。