なぜ関数があるのか
関数とは、呼び出すことで好きなときに実行できる、名前のついたコードのまとまりです。同じロジックを何か所も繰り返し書く代わりに、一度だけ書いて名前を付け、必要な場所でその名前を呼び出します。こうするとプログラムは短く、読みやすく、直しやすくなります。ロジックを一か所変えれば、すべての呼び出し元がその更新を受け取れるのです。
実はあなたはこれまでずっと、ある関数を呼び出してきました。それが main です。これはすべての C++ プログラムが開始する入口です。これからは自分の関数を書きます。以前に見た 範囲ベース for のようなループは、しばしば関数の中に置かれ、ひとまとまりのロジックを名前で再利用できるようになっています。
関数の構造
どの関数にも 4 つの部分があります。戻り値の型、名前、括弧で囲んだ引数リスト、そして波括弧で囲んだ本体です。
int add(int a, int b) { // 戻り値の型 | 名前 | 引数
return a + b; // 本体
}
intは戻り値の型で、関数が返す値の種類です。addは呼び出すときに使う名前です。(int a, int b)は引数で、呼び出し元が渡す入力です。- 波括弧は本体、つまり呼び出したときに実行されるコードを保持します。
これを完全なプログラムにしたものが次です。関数を main より上に定義すると、main が呼び出すときにそれが見えます。
add(2, 3) という呼び出しは、a = 2、b = 3 で関数を実行し、その式全体が返された値になります。それを変数に格納してもよいし、2 つ目の cout の行のように別の式の中で直接使ってもかまいません。
値を返す
return 文は 2 つのことを行います。呼び出し元に値を返し、そして即座に関数を終了させます。return の後にあるコードは実行されません。制御は関数が呼び出された場所へまっすぐ戻ります。
返す値の型は、宣言した戻り値の型と一致する(あるいはそれに変換できる)必要があります。int として宣言した関数は int を返すべきです。何も返さなかったり、return なしで末尾を抜けてしまったりするのは、void でないすべての関数にとってバグです。
void 関数
すべての関数が値を生み出すわけではありません。関数がただ何かを行うだけ(出力を表示する、状態を更新するなど)のとき、その戻り値の型は void になります。void 関数は、早く抜けるために単独の return; を使うことも、単に閉じ波括弧まで実行することもできます。
void 関数の結果を使おうとする(int x = greet("Ada");)のはコンパイルエラーになります。代入できる値が存在しないからです。よくあるミスは、void 関数の中で return someValue; と書くことです。これもコンパイラは拒否します。
宣言と定義
C++ はファイルを上から下へ読むので、既定では関数はそれを呼び出すコードより前に現れていなければなりません。その順序が都合の悪いときは、関数を宣言(プロトタイプとも呼ばれます)と定義に分けます。
宣言は関数のシグネチャを述べ、セミコロンで終わります。本体はありません。コンパイラに「この関数は存在します。呼び出し方はこうです」と約束するのです。完全な定義はその後に、main の後ですら置けます。
4 行目のプロトタイプがなければ、コンパイラは square を一度も見ないうちに main 内の square(5) に出くわし、ビルドは失敗します。プロトタイプはまた、ヘッダファイルが多くのソースファイルで同じ関数を共有できるようにする仕組みでもあります。宣言における引数の名前は省略可能であることに注意してください。int square(int); でも同じように機能します。コンパイラにとって重要なのは型だけです。
よくあるミス
いくつかの罠が、初心者を何度も引っかけます。
- 宣言する前に呼び出す。 「
addwas not declared in this scope」というエラーが出たら、その関数は最初の呼び出しより下で定義されていて、プロトタイプがありません。定義を上へ移すか、プロトタイプを追加しましょう。 - return を忘れる。
voidでない関数の末尾にreturnなしで到達するのは未定義動作で、呼び出し元はゴミを受け取ります。警告を有効にして(-Wall)コンパイルすれば、コンパイラがそれを指摘してくれます。 - 定義と呼び出しの取り違え。 定義には波括弧で囲んだ本体があり、末尾のセミコロンはありません。宣言にはセミコロンがあり、本体はありません。これらを取り違えると——例えば定義するつもりの関数の引数リストの直後にセミコロンを置くと——分かりにくいエラーが出ます。
- 戻り値を無視する。
add(2, 3);だけを 1 行に書いてもコンパイルは通りますが、計算された合計は静かに捨てられます。関数が返すものを実際に使っているか確かめましょう。
// 定義のように見えますが、はぐれた ; のせいでこれは宣言になり、
// その後に取り残されたブロックが続きます — よくあるタイプミスです:
int triple(int n); // <- この ; が文を終わらせる
{
return n * 3; // ここでは n は未定義;このブロックは孤立してしまった
}
次へ: 関数の引数
関数が引数リストを通して入力を受け取る様子を見てきましたが、話はそれだけではありません。次のページでは関数の引数を掘り下げます。値渡しと参照渡し、デフォルト引数、const 引数、そしてその選択が、関数が呼び出し元のデータを変更できるかどうかにどう影響するか、です。
よくある質問
C++ で関数はどう書きますか?
戻り値の型、名前、引数を入れる括弧、そして波括弧で囲んだ本体を与えます: int add(int a, int b) { return a + b; }。add(2, 3) のように名前と引数を付けて呼び出します。関数が何も返さない場合は、戻り値の型に void を使います。
C++ における関数の宣言と定義の違いは何ですか?
宣言(プロトタイプ)は、関数の名前・戻り値の型・引数をコンパイラに伝えるもので、セミコロンで終わります: int add(int a, int b);。定義はそれに加えて波括弧で囲んだ本体も提供します。関数を main より前に宣言し、後で定義することもできます。宣言があれば、定義が現れる前にその関数を呼び出せます。
C++ の関数が値を返さなかったらどうなりますか?
void 関数なら何も起きず、単に終了します。しかし void でない関数では、return なしで末尾に到達するのは未定義動作です。呼び出し側はゴミの値を受け取り、プログラムが正しく動かなくなることがあります。ほとんどのコンパイラはこれを警告します。void でない関数では、すべての経路で必ず値を返しましょう。