よく使う処理は関数にしよう:目指せ! Cプログラマ(8)(2/2 ページ)
繰り返し登場する処理を1つにまとめる関数を定義することで、プログラムの保守性が高まり、開発効率も良くなります。
main関数
サンプルプログラムでこれまで使ってきたmain関数も、ほかの関数と同じように関数の1つです。ただしmain関数をCプログラマが自分で呼び出す必要はなく、プログラムが実行されたときに、自動的に呼び出されるようになっています。
main関数は次のいずれかの形になります。
- int main(void)
- int main(int argc, char * argv[])
- 上の2つ以外に定義された、処理系独自の方法
3つめのものについては処理系ごとに異なりますので、コンパイラのマニュアルを参照してください。ここでは上の2つについて説明します。
1つ目のmain関数は、引数がなく、int型の戻り値を持つ関数です。Hello Worldのプログラムを確認してみましょう。
#include <stdio.h> #include <stdlib.h> int main(void) { puts("!!!Hello World!!!"); /* prints !!!Hello World!!! */ return EXIT_SUCCESS; }
4行目からmain関数の定義が始まり、6行目でEXIT_SUCCESSというものを返しています。main関数からの戻り値は、このプログラムを実行した実行環境(ホスト環境)に返します。Pleiadesで実行した場合はWindowsが実行環境になりますので、EXIT_SUCCESSはWindowsに戻される値になります。
このEXIT_SUCCESSは“マクロ”と呼ばれる機能で値が定義されています。詳しくは今後の記事で説明しますが、最終的には成功終了を示すint型の値に置き換えられます。成功終了を示すint型の値とは0のことなので、「return EXIT_SUCCESS;」の代わりに「return 0;」と書いても同じことになります。
この戻り値がどのように扱われるかは、プログラムの呼び出し方によって決まります。例えば、何かのプログラムからこのプログラムを呼び出した場合は、呼び出し元に値が返るでしょう。また、Windowsのコマンドプロンプト上で実行されたときは、ERRORLEVELに入れられます。
プログラムを作っているときには、そのプログラムがどのように呼び出されるかは分かりませんから、どのような呼び出され方をしても問題ないように適切な値を返すようにしておくべきです。正常に処理が終わったのであればEXIT_SUCCESSを、エラーならばエラーを示す値を返すようにしましょう。
2つ目のmain関数は、戻り値の型こそ1つ目と同じですが、引数を2つ取る点が異なります。1つ目はint型の値、2つ目はchar *[]型です。これらにはプログラムへ渡された引数の数と、その値が入ります。これらの扱い方についてはポインタの知識が必要ですので、今後の記事で解説します。
関数プロトタイプ
定義した関数はどこからでも呼び出すことができますが、呼び出す前に関数の呼び出し方を宣言しておく必要があります。呼び出す前、というのは、プログラムファイルにおいて関数を呼び出すより前ということです。
まず、呼び出し方の宣言が前にない例を見てみましょう。
int main(void) { int x; x = function(); // ここで関数functionを呼び出しているが... printf("function returns %d.\n", x); } // 関数functionの定義は後ろにある。 int function(void) { printf("function called.\n"); return 0; }
このコードをコンパイルすると関数を呼び出している行で「implicit declaration of function `function'」(関数functionの暗黙の宣言)という警告が出ます。以前のCには暗黙の関数宣言をする機能(関数の呼び出し方の宣言がない場合はint型と宣言されたものとみなす機能)というものがありましたが、この機能はC99でなくなっています。Cのコンパイル時チェックを有効に利用するため、なるべくきちんと宣言するようにしましょう。
関数の呼び出し方をきちんと宣言すると、次のようになります。
int main(void) { int x; int function(void); // この行を追加した。 x = function(); // 今度は問題なし! printf("function returns %d.\n", x); } // funcの定義は後ろにある。 int function(void) { printf("function called.\n"); return 0; }
宣言の1行を追加して再度コンパイルしてみると、今度は警告が出ません。
この関数の呼び出し方を記した書き方を“関数プロトタイプ(関数原型)”と呼びます。つまり、関数呼び出しより前に関数プロトタイプを宣言しておくことで、その関数の呼び出し方が分かるようになっています。
関数プロトタイプの宣言は、関数やブロックの中や、関数の外側にも書くことができます。関数はいろいろなところから利用できるようにしたい場合が多いので、プロトタイプ宣言は基本的に関数の外側ですると覚えておきましょう。関数の中に書くとその宣言はその関数の中だけで有効になります。ある関数の中でしか使わない関数を用意するときはこういった方法もとれると頭の隅に覚えておけば十分です。
なお、関数本体の宣言は“関数定義の宣言”と呼び、関数定義の宣言は関数プロトタイプの宣言と兼ねることができます。前のサンプルで先に関数定義の宣言が書いてある場合に問題にならなかったのはこのためです。
// 関数定義の宣言。関数プロトタイプの宣言を兼ねている。 int adder(int a1, int a2) { return a1 + a2; } int main(void) { // 問題なし。 int x = adder(1, 2); }
inline
inlineはC99から追加されたキーワードの1つです。関数の宣言の前に付けて使います。inlineが付いた関数は“インライン関数”と呼ばれます。
// 関数adderをインライン関数にした例。 inline int adder(int a1, int a2);
インライン関数にすると、関数の呼び出し処理が高速化される場合があります。何も変わらない場合もあります。というのも、インライン関数がどのように処理されるか、あるいは単に無視されるかは、コンパイラ次第だからです。
とは言っても実際には想定されている処理方法があり、それは“インライン展開”(あるいは“インライン置換”)と呼ばれるものです。
関数を呼び出すためには、引数を準備したり、戻ってくる場所を覚えておいたり、戻り値を処理したりというように、いくつもの処理が必要です。こういった処理については、プログラマから見えないところでコンパイラがバイナリ生成時に対応してくれています。この手間をできる限り省いて、関数の中身を呼び出ししたところに展開してしまおう、というのがインライン展開です。
しかしインライン展開の処理については、おそらく皆さんが想像する以上に複雑な(おもしろい?)扱われ方をされます。組み込みソフトウェアの開発などでは処理の高速化方法としてinlineが紹介されることがありますが、単にinlineにすれば必ず速くなるかというとそうばかりでもなく、場合によっては逆に遅くなってしまう場合もあります。
そのため、むやみにinlineを指定するのはいけません。利用しているコンパイラでinlineがどのように扱われるか調べた上、必要なところだけに指定するようにしましょう。調べる手間が取れないようであれば、指定しない方が賢明です。
オペランドの評価順序
ここで関数の話題から少し離れて、オペランドの評価順序について説明したいと思います。
オペランドは、演算子の演算対象のことでした(第4回を参照)。演算子には優先順位があって、優先順位の高いものから評価されることは以前に説明しましたが、オペランドの評価順序についても考慮が必要になってきます。
というのも、実はオペランドの評価順序は一部の演算子を除いて決まっていないからです。決まっていないということは、あるコンパイラと別のコンパイラで、順序が違う可能性があるということです。
次の例を見てください。
int n; int inc3(void) { n += 3; return n; } int mul3(void) { n *= 3; return n; } int main(void) { n = 1; int x = inc3() + mul3(); printf("%d\n", x); // 16 か、それとも 9 か。 return EXIT_SUCCESS; }
この例では変数nが関数の外側で宣言されています。このような書き方をすると、複数の関数で同じ変数を参照することができるようになります。これについては次回の記事で説明します。
関数呼び出し演算子は加減演算子より優先順位が高いので、main関数ではinc3関数の結果とmul3関数の結果を足し合わせて表示する、という処理になります。
ところが、加減演算子のオペランドであるinc3関数とmul3関数は、呼び出し順序が決まっていません。このような書き方をすると、どちらが先に呼び出されるか分からないのです。
しかもこのプログラムは、どちらの関数が先に呼び出されるかによって結果が変わります。
a. inc3、mul3の順で呼び出される場合 (1) inc3が呼び出され、n = 1 + 3 となり、4 が返る。 (2) mul3が呼び出され、n = 4 * 3 となり、12 が返る。 (3) x = 4 + 12、つまり16が表示される。 b. mul3、inc3の順で呼び出される場合 (1) mul3が呼び出され、n = 1 * 3 となり、3 が返る。 (2) inc3が呼び出され、n = 3 + 3 となり、6 が返る。 (3) x = 3 + 6、つまり9が表示される。
このようなプログラムはコンパイルが問題なく通って、もし正確に実行できたとしても、大変問題のあるプログラムです。後になって、コンパイラをバージョンアップしたり、コンパイルオプションを変えた瞬間に、呼び出し順序が変わってしまうかもしれません。
inc3、mul3の順で確実に呼び出したいのであれば、「x = inc3(); x += mul3();」のように、2つの文に分けることで実現できます。
なお、似たようなコードでも次のようなコードは、「1つの式の中では、1つの変数の値を変更できるのは1回に限る(*4)」というルールから、コンパイル エラーになります。
int n = 1; int x = (n += 3) + (n *= 3);
(*4) 第5回で説明しました。
また、関数の引数の評価順序も決まっていません。
int n; void func(int a, int b) { /* ... */ } int main(void) { n = 1; func(inc3(), mul3()); // func(4, 12) になるか func(6, 3) になるか分からない。 return EXIT_SUCCESS; }
これも合わせて覚えておきましょう。
ただし、次に示す演算子については、オペランドの評価順序が決まっています。
- &&(論理AND演算子)
左オペランドを先に評価する。左オペランドが0(偽)の場合は右オペランドを評価しないし、そうでなければ評価する。 - ||(論理OR演算子)
左オペランドを先に評価する。左オペランドが0以外(真)の場合は右オペランドを評価しないし、そうでなければ評価する。 - ?:(条件演算子)
第1オペランドを先に評価する。第1オペランドが0以外(真)ならば第2オペランドだけを評価し、0(偽)ならば第3オペランドだけを評価する。 - , (コンマ演算子)
左オペランドを先に評価する。次に右オペランドを評価する。
// f, g, hは、いずれも引数を持たずint型の値を返す関数。 int f(void) { return /* ... */; } f() && g(); // fが0以外を返したときだけgが呼び出される。 f() || g(); // fが0を返したときだけgが呼び出される。 f() ? g() : h(); // fが0以外を返すとgが、0を返すとhが呼び出される。 f() , g(); // fが呼び出され、次にgが呼び出される。
論理AND演算子や論理OR演算子は、左オペランドを評価した段階で結果が決まってしまうときには、右オペランドを評価しません。評価しないということは「計算はするけれど結果は捨てる」といった意味ではなく、「計算すらしない」ということです。
今回学んだこと
- 関数の定義は「入力、処理、出力」で決まります。
- 関数の引数と戻り値について学びました。
- voidキーワードについて学びました。
- main関数は特別な関数であることを学びました。
- 関数を使う前に、関数プロトタイプの宣言が必要なことを学びました。
- inlineキーワードについて学びました。
- 思ったとおりの計算をするためには、オペランドの評価順序を意識したコーディングが必要であることを学びました。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- いまさらアルゴリズムを学ぶ意味
コーディングに役立つ! アルゴリズムの基本(1) コンピュータに「3の倍数と3の付く数字」を判断させるにはどうしたらいいか。発想力を鍛えよう - Zope 3の魅力に迫る
Zope 3とは何ぞや?(1) Pythonで書かれたWebアプリケーションフレームワーク「Zope 3」。ほかのソフトウェアとは一体何が違っているのか? - 貧弱環境プログラミングのススメ
柴田 淳のコーディング天国 高性能なIT機器に囲まれた環境でコンピュータの動作原理に触れることは可能だろうか。貧弱なPC上にビットマップの直線をどうやって引く? - Haskellプログラミングの楽しみ方
のんびりHaskell(1) 関数型言語に分類されるHaskell。C言語などの手続き型言語とまったく異なるプログラミングの世界に踏み出してみよう - ちょっと変わったLisp入門
Gaucheでメタプログラミング(1) Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう