第3章: 関数と参照

プログラムを構成する基本的な部品である「関数」について、C++ならではの引数の渡し方や便利な機能を学びます。他の言語で関数に慣れている方も、C++特有の概念である「参照」は特に重要なので、しっかり理解していきましょう。

関数の基本: 宣言と定義の分離

プログラム内の特定のタスクを実行するコードのまとまりを「関数」と呼びます。C++では、この関数を利用する前に、コンパイラがその関数の存在と使い方を知っている必要があります。そのため、「宣言 (declaration)」と「定義 (definition)」という2つの概念が重要になります。

  • 宣言 (Declaration): 関数の使い方(名前、引数、戻り値)をコンパイラに教える。本体の処理はない。
  • 定義 (Definition): 関数の具体的な処理内容を記述する。

関数の宣言

宣言の基本的な文法は以下の通りです。

戻り値の型 関数名(引数の型1 引数名1, 引数の型2 引数名2, ...);
  • 戻り値の型 (Return Type): 関数が処理を終えた後に返す値の型です。例えば、int型なら整数値を返します。
  • 関数名 (Function Name): 関数を呼び出すときに使う名前です。
  • 引数リスト (Parameter List): 関数が処理のために受け取る値です。()の中に、型名 変数名のペアをコンマで区切って記述します。引数が必要ない場合は () の中を空にします。
  • セミコロン (;): 宣言の最後には必ずセミコロンを付けます。

戻り値がない場合: `void`型

関数が何も値を返す必要がない場合もあります。例えば、「画面にメッセージを表示するだけ」といった関数です。その場合、戻り値の型として void という特別なキーワードを使います。

void printMessage(std::string message);

第2章で学んだように、intdoubleなどの型は変数を定義するために使えましたが、voidは「型がない」ことを示す特殊な型なので、void my_variable; のように変数を定義することはできません。あくまで関数の戻り値の型としてのみ使います。

コンパイラは上から順に読む

C++のコンパイラはソースコードを上から下へと順番に読み込んでいきます。そのため、main関数のような場所で別の関数を呼び出すコードに出会ったとき、コンパイラはその時点ですでに関数の「宣言」または「定義」を読み込んでいる必要があります。

つまり、main関数よりも上(前)に、呼び出す関数の定義か宣言のどちらかが書かれていなければコンパイルエラーになります。

コードを整理するため、一般的にはmain関数の前に関数の「宣言」だけを記述し、main関数の後(または別のファイル)に具体的な処理内容である「定義」を記述するスタイルがよく使われます。

以下の例で確認してみましょう。

declaration_definition.cpp

この例では、main関数が始まる前にgreet関数とadd関数の宣言をしています。これにより、main関数内でこれらの関数を自由な順序で呼び出すことができ、コードの可読性が向上します。関数の具体的な実装はmain関数の後にまとめて記述することで、「プログラムの全体的な流れ(main)」と「各部分の具体的な処理(関数の定義)」を分離して考えることができます。

引数の渡し方

C++の関数の引数の渡し方には、主に 「値渡し」「ポインタ渡し」「参照渡し」 の3つがあります。ここでは特にC++特有の「参照渡し」に注目します。

値渡し (Pass by Value)

引数に渡された値がコピーされて、関数内のローカル変数として扱われます。関数内でその値を変更しても、呼び出し元の変数は影響を受けません。これは多くの言語で標準的な引数の渡し方です。

pass_by_value.cpp
  • 長所: 呼び出し元の変数が不用意に書き換えられることがなく、安全です。
  • 短所: 大きなオブジェクト(例えば、たくさんの要素を持つ std::vector など)を渡すと、コピーのコストが無視できなくなり、パフォーマンスが低下する可能性があります。

ポインタ渡し (Pass by Pointer)

これはC言語から引き継がれた伝統的な方法で、変数のメモリアドレスを渡します。ポインタ(アドレスを指し示す変数)を介して、呼び出し元の変数を直接変更できます。詳細は第4章で詳しく学びますが、ここでは簡単に紹介します。

pass_by_pointer.cpp

ポインタは強力ですが、nullptr(どこも指していないポインタ)の可能性を考慮する必要があるなど、扱いが少し複雑です。

参照渡し (Pass by Reference)

C++の大きな特徴の一つが参照 (Reference) です。参照は、既存の変数に別名を付ける機能と考えることができます。。

関数に参照を渡すと、値のコピーは発生せず、関数内の引数は呼び出し元の変数の「別名」として振る舞います。そのため、関数内での操作が呼び出し元の変数に直接反映されます。構文もポインタよりずっとシンプルです。

pass_by_reference.cpp
  • 長所: コピーが発生しないため効率的です。また、構文がシンプルで、呼び出し元の変数を変更する意図が明確になります。
  • 注意点: 関数内で値を変更できるため、意図しない書き換えに注意が必要です。

`const`参照: 効率と安全性の両立

「大きなオブジェクトを渡したいけど、コピーは避けたい。でも関数内で値を変更されたくはない」という場合に最適なのが const参照 です。

const_reference.cpp

C++のベストプラクティス:

  • intdoubleなどの小さな基本型は値渡し
  • 関数内で引数を変更する必要がある場合は参照渡し (&)
  • 大きなオブジェクトを渡すが変更はしない場合は const参照 (const &) を使う。

関数のオーバーロード

C++では、同じ名前で引数の型や個数が異なる関数を複数定義できます。これをオーバーロード (Overload) と呼びます。コンパイラは、関数呼び出し時の引数の型や個数を見て、どの関数を呼び出すべきかを自動的に判断してくれます。

overloading.cpp

これにより、printInt, printDouble のように別々の名前を付ける必要がなくなり、コードが直感的で読みやすくなります。

注意点として、戻り値の型が違うだけではオーバーロードはできません。あくまで引数のリストが異なる必要があります。

デフォルト引数

関数の引数に、あらかじめデフォルト値を設定しておくことができます。これにより、関数を呼び出す際に該当する引数を省略できるようになります。

デフォルト引数は、引数リストの右側から設定する必要があります。一度デフォルト引数を設定したら、それより右側にある引数もすべてデフォルト引数を持たなければなりません。

default_arguments.cpp

この章のまとめ

この章では、C++の関数に関する基本的ながらも重要な機能を学びました。

  • 関数の宣言と定義の分離: プログラムの構造を整理し、分割コンパイルを可能にするための基本です。
  • 引数の渡し方:
    • 値渡し: 引数のコピーを作成し、元の変数を保護します。
    • 参照渡し (&): 変数の「別名」を渡し、コピーのコストをなくします。呼び出し元の変数を変更するため、または効率化のために使います。
    • const参照渡し (const&): 効率的でありながら、意図しない変更を防ぐためのC++の定石です。
  • 関数のオーバーロード: 同じ名前で引数リストの異なる関数を定義でき、文脈に応じた適切な関数が自動で選ばれます。
  • デフォルト引数: 関数の引数を省略可能にし、柔軟な関数呼び出しを実現します。

特に「参照」は、この先のC++プログラミングで頻繁に登場する極めて重要な概念です。値渡しとの違い、そしてconst参照との使い分けをしっかりマスターしましょう。

練習問題1: 値の交換

2つのint型変数の値を交換する関数 swap を作成してください。この関数は、呼び出し元の変数の値を直接変更できるように、参照渡しを使って実装してください。

practice3_1.cpp

問題2: 図形の面積

関数のオーバーロードを使い、正方形と長方形の面積を計算する calculate_area という名前の関数を実装してください。

  1. 引数が1つ (int side) の場合は、正方形の面積 (side✕side) を計算して返す。
  2. 引数が2つ (int width, int height) の場合は、長方形の面積 (width✕height) を計算して返す。

作成した関数をmain関数から呼び出し、結果が正しく表示されることを確認してください。

practice3_2.cpp