|
| 1 | +# 第3章: 関数と参照 |
| 2 | + |
| 3 | +プログラムを構成する基本的な部品である「関数」について、C++ならではの引数の渡し方や便利な機能を学びます。他の言語で関数に慣れている方も、C++特有の概念である「参照」は特に重要なので、しっかり理解していきましょう。 |
| 4 | + |
| 5 | +## 関数の基本: 宣言と定義の分離 |
| 6 | + |
| 7 | +プログラム内の特定のタスクを実行するコードのまとまりを「**関数**」と呼びます。C++では、この関数を利用する前に、コンパイラがその関数の存在と使い方を知っている必要があります。そのため、「**宣言 (declaration)**」と「**定義 (definition)**」という2つの概念が重要になります。 |
| 8 | + |
| 9 | + * **宣言 (Declaration)**: 関数の使い方(名前、引数、戻り値)をコンパイラに教える。本体の処理はない。 |
| 10 | + * **定義 (Definition)**: 関数の具体的な処理内容を記述する。 |
| 11 | + |
| 12 | +### 関数の宣言 |
| 13 | + |
| 14 | +宣言の基本的な文法は以下の通りです。 |
| 15 | + |
| 16 | +``` |
| 17 | +戻り値の型 関数名(引数の型1 引数名1, 引数の型2 引数名2, ...); |
| 18 | +``` |
| 19 | + |
| 20 | + * **戻り値の型 (Return Type)**: 関数が処理を終えた後に返す値の型です。例えば、`int`型なら整数値を返します。 |
| 21 | + * **関数名 (Function Name)**: 関数を呼び出すときに使う名前です。 |
| 22 | + * **引数リスト (Parameter List)**: 関数が処理のために受け取る値です。`()`の中に、`型名 変数名`のペアをコンマで区切って記述します。引数が必要ない場合は `()` の中を空にします。 |
| 23 | + * **セミコロン (`;`)**: 宣言の最後には必ずセミコロンを付けます。 |
| 24 | + |
| 25 | +### 戻り値がない場合: `void`型 |
| 26 | + |
| 27 | +関数が何も値を返す必要がない場合もあります。例えば、「画面にメッセージを表示するだけ」といった関数です。その場合、戻り値の型として `void` という特別なキーワードを使います。 |
| 28 | + |
| 29 | +```cpp |
| 30 | +void printMessage(std::string message); |
| 31 | +``` |
| 32 | +
|
| 33 | +第2章で学んだように、`int`や`double`などの型は変数を定義するために使えましたが、`void`は「型がない」ことを示す特殊な型なので、`void my_variable;` のように変数を定義することはできません。あくまで関数の戻り値の型としてのみ使います。 |
| 34 | +
|
| 35 | +### コンパイラは上から順に読む |
| 36 | +
|
| 37 | +C++のコンパイラはソースコードを上から下へと順番に読み込んでいきます。そのため、`main`関数のような場所で別の関数を呼び出すコードに出会ったとき、コンパイラはその時点ですでに関数の「宣言」または「定義」を読み込んでいる必要があります。 |
| 38 | +
|
| 39 | +つまり、**`main`関数よりも上(前)に、呼び出す関数の定義か宣言のどちらかが書かれていなければコンパイルエラー**になります。 |
| 40 | +
|
| 41 | +コードを整理するため、一般的には`main`関数の前に関数の「宣言」だけを記述し、`main`関数の後(または別のファイル)に具体的な処理内容である「定義」を記述するスタイルがよく使われます。 |
| 42 | +
|
| 43 | +以下の例で確認してみましょう。 |
| 44 | +
|
| 45 | +```cpp:declaration_definition.cpp |
| 46 | +#include <iostream> |
| 47 | +#include <string> |
| 48 | +
|
| 49 | +// 1. 関数の「宣言」(プロトタイプ宣言) |
| 50 | +// これにより、main関数の中で greet や add を使ってもコンパイラはエラーを出さない。 |
| 51 | +void greet(std::string name); // 戻り値がない関数の宣言 |
| 52 | +int add(int a, int b); // int型の値を返す関数の宣言 |
| 53 | +
|
| 54 | +// main関数: プログラムの開始点 |
| 55 | +int main() { |
| 56 | + // 宣言があるので、これらの関数を呼び出すことができる |
| 57 | + greet("Taro"); |
| 58 | +
|
| 59 | + int result = add(5, 3); |
| 60 | + std::cout << "5 + 3 = " << result << std::endl; |
| 61 | + |
| 62 | + return 0; |
| 63 | +} |
| 64 | +
|
| 65 | +// 2. 関数の「定義」(本体の実装) |
| 66 | +// 実際の処理はここに書く。 |
| 67 | +void greet(std::string name) { |
| 68 | + std::cout << "Hello, " << name << "!" << std::endl; |
| 69 | +} |
| 70 | +
|
| 71 | +int add(int a, int b) { |
| 72 | + return a + b; |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +```cpp-exec:declaration_definition.cpp |
| 77 | +Hello, Taro! |
| 78 | +5 + 3 = 8 |
| 79 | +``` |
| 80 | + |
| 81 | +この例では、`main`関数が始まる前に`greet`関数と`add`関数の宣言をしています。これにより、`main`関数内でこれらの関数を自由な順序で呼び出すことができ、コードの可読性が向上します。関数の具体的な実装は`main`関数の後にまとめて記述することで、「プログラムの全体的な流れ(`main`)」と「各部分の具体的な処理(関数の定義)」を分離して考えることができます。 |
| 82 | + |
| 83 | +## 引数の渡し方 |
| 84 | + |
| 85 | +C++の関数の引数の渡し方には、主に **「値渡し」「ポインタ渡し」「参照渡し」** の3つがあります。ここでは特にC++特有の「参照渡し」に注目します。 |
| 86 | + |
| 87 | +### 値渡し (Pass by Value) |
| 88 | + |
| 89 | +引数に渡された値が**コピー**されて、関数内のローカル変数として扱われます。関数内でその値を変更しても、呼び出し元の変数は影響を受けません。これは多くの言語で標準的な引数の渡し方です。 |
| 90 | + |
| 91 | +```cpp:pass_by_value.cpp |
| 92 | +#include <iostream> |
| 93 | + |
| 94 | +void tryToChange(int x) { |
| 95 | + x = 100; // 関数内のコピーが変更されるだけ |
| 96 | + std::cout << "Inside function: x = " << x << std::endl; |
| 97 | +} |
| 98 | + |
| 99 | +int main() { |
| 100 | + int my_number = 10; |
| 101 | + std::cout << "Before function call: my_number = " << my_number << std::endl; |
| 102 | + tryToChange(my_number); |
| 103 | + std::cout << "After function call: my_number = " << my_number << std::endl; // 10のまま変わらない |
| 104 | + return 0; |
| 105 | +} |
| 106 | +``` |
| 107 | +
|
| 108 | +```cpp-exec:pass_by_value.cpp |
| 109 | +Before function call: my_number = 10 |
| 110 | +Inside function: x = 100 |
| 111 | +After function call: my_number = 10 |
| 112 | +``` |
| 113 | + |
| 114 | + * **長所**: 呼び出し元の変数が不用意に書き換えられることがなく、安全です。 |
| 115 | + * **短所**: 大きなオブジェクト(例えば、たくさんの要素を持つ `std::vector` など)を渡すと、コピーのコストが無視できなくなり、パフォーマンスが低下する可能性があります。 |
| 116 | + |
| 117 | +### ポインタ渡し (Pass by Pointer) |
| 118 | + |
| 119 | +これはC言語から引き継がれた伝統的な方法で、変数のメモリアドレスを渡します。ポインタ(アドレスを指し示す変数)を介して、呼び出し元の変数を直接変更できます。詳細は第4章で詳しく学びますが、ここでは簡単に紹介します。 |
| 120 | + |
| 121 | +```cpp:pass_by_pointer.cpp |
| 122 | +#include <iostream> |
| 123 | + |
| 124 | +// ポインタを受け取るには、型名の後にアスタリスク * を付ける |
| 125 | +void changeWithPointer(int* ptr) { |
| 126 | + *ptr = 100; // アスタリスク * でポインタが指す先の値にアクセス |
| 127 | +} |
| 128 | + |
| 129 | +int main() { |
| 130 | + int my_number = 10; |
| 131 | + // 変数のアドレスを渡すには、アンパサンド & を付ける |
| 132 | + changeWithPointer(&my_number); |
| 133 | + std::cout << "After function call: my_number = " << my_number << std::endl; // 100に変わる |
| 134 | + return 0; |
| 135 | +} |
| 136 | +``` |
| 137 | +
|
| 138 | +```cpp-exec:pass_by_pointer.cpp |
| 139 | +After function call: my_number = 100 |
| 140 | +``` |
| 141 | + |
| 142 | +ポインタは強力ですが、`nullptr`(どこも指していないポインタ)の可能性を考慮する必要があるなど、扱いが少し複雑です。 |
| 143 | + |
| 144 | +### 参照渡し (Pass by Reference) |
| 145 | + |
| 146 | +C++の大きな特徴の一つが**参照 (Reference)** です。参照は、既存の変数に**別名**を付ける機能と考えることができます。。 |
| 147 | + |
| 148 | +関数に参照を渡すと、値のコピーは発生せず、関数内の引数は呼び出し元の変数の「別名」として振る舞います。そのため、関数内での操作が呼び出し元の変数に直接反映されます。構文もポインタよりずっとシンプルです。 |
| 149 | + |
| 150 | +```cpp:pass_by_reference.cpp |
| 151 | +#include <iostream> |
| 152 | + |
| 153 | +// 参照を受け取るには、型名の後にアンパサンド & を付ける |
| 154 | +void changeWithReference(int& ref) { |
| 155 | + ref = 100; // 通常の変数と同じように扱うだけ |
| 156 | +} |
| 157 | + |
| 158 | +int main() { |
| 159 | + int my_number = 10; |
| 160 | + changeWithReference(my_number); // 呼び出し側は特別な記号は不要 |
| 161 | + std::cout << "After function call: my_number = " << my_number << std::endl; // 100に変わる |
| 162 | + return 0; |
| 163 | +} |
| 164 | +``` |
| 165 | +
|
| 166 | +```cpp-exec:pass_by_reference.cpp |
| 167 | +After function call: my_number = 100 |
| 168 | +``` |
| 169 | + |
| 170 | + |
| 171 | + * **長所**: コピーが発生しないため効率的です。また、構文がシンプルで、呼び出し元の変数を変更する意図が明確になります。 |
| 172 | + * **注意点**: 関数内で値を変更できるため、意図しない書き換えに注意が必要です。 |
| 173 | + |
| 174 | +#### `const`参照: 効率と安全性の両立 |
| 175 | + |
| 176 | +「大きなオブジェクトを渡したいけど、コピーは避けたい。でも関数内で値を変更されたくはない」という場合に最適なのが **`const`参照** です。 |
| 177 | + |
| 178 | +```cpp:const_reference.cpp |
| 179 | +#include <iostream> |
| 180 | +#include <string> |
| 181 | + |
| 182 | +// const参照で受け取ることで、コピーを防ぎつつ、 |
| 183 | +// messageが関数内で変更されないことを保証する |
| 184 | +void printMessage(const std::string& message) { |
| 185 | + // message = "changed!"; // この行はコンパイルエラーになる! |
| 186 | + std::cout << message << std::endl; |
| 187 | +} |
| 188 | + |
| 189 | +int main() { |
| 190 | + std::string greeting = "Hello, C++ world! This is a long string."; |
| 191 | + printMessage(greeting); |
| 192 | + return 0; |
| 193 | +} |
| 194 | +``` |
| 195 | +
|
| 196 | +```cpp-exec:const_reference.cpp |
| 197 | +Hello, C++ world! This is a long string. |
| 198 | +``` |
| 199 | + |
| 200 | +**C++のベストプラクティス**: |
| 201 | + |
| 202 | + * `int`や`double`などの小さな基本型は**値渡し**。 |
| 203 | + * 関数内で引数を**変更する必要がある**場合は**参照渡し (`&`)**。 |
| 204 | + * 大きなオブジェクトを渡すが**変更はしない**場合は **`const`参照 (`const &`)** を使う。 |
| 205 | + |
| 206 | +## 関数のオーバーロード |
| 207 | + |
| 208 | +C++では、**同じ名前で引数の型や個数が異なる関数を複数定義**できます。これを**オーバーロード (Overload)** と呼びます。コンパイラは、関数呼び出し時の引数の型や個数を見て、どの関数を呼び出すべきかを自動的に判断してくれます。 |
| 209 | + |
| 210 | +```cpp:overloading.cpp |
| 211 | +#include <iostream> |
| 212 | +#include <string> |
| 213 | + |
| 214 | +// int型の引数を1つ取るprint関数 |
| 215 | +void print(int value) { |
| 216 | + std::cout << "Integer: " << value << std::endl; |
| 217 | +} |
| 218 | + |
| 219 | +// string型の引数を1つ取るprint関数 |
| 220 | +void print(const std::string& value) { |
| 221 | + std::cout << "String: " << value << std::endl; |
| 222 | +} |
| 223 | + |
| 224 | +// double型とint型の引数を取るprint関数 |
| 225 | +void print(double d_val, int i_val) { |
| 226 | + std::cout << "Double: " << d_val << ", Integer: " << i_val << std::endl; |
| 227 | +} |
| 228 | + |
| 229 | +int main() { |
| 230 | + print(123); |
| 231 | + print("hello"); |
| 232 | + print(3.14, 42); |
| 233 | + return 0; |
| 234 | +} |
| 235 | +``` |
| 236 | +
|
| 237 | +```cpp-exec:overloading.cpp |
| 238 | +Integer: 123 |
| 239 | +String: hello |
| 240 | +Double: 3.14, Integer: 42 |
| 241 | +``` |
| 242 | + |
| 243 | +これにより、`printInt`, `printDouble` のように別々の名前を付ける必要がなくなり、コードが直感的で読みやすくなります。 |
| 244 | + |
| 245 | +注意点として、戻り値の型が違うだけではオーバーロードはできません。あくまで引数のリストが異なる必要があります。 |
| 246 | + |
| 247 | +## デフォルト引数 |
| 248 | + |
| 249 | +関数の引数に、あらかじめ**デフォルト値**を設定しておくことができます。これにより、関数を呼び出す際に該当する引数を省略できるようになります。 |
| 250 | + |
| 251 | +デフォルト引数は、引数リストの**右側**から設定する必要があります。一度デフォルト引数を設定したら、それより右側にある引数もすべてデフォルト引数を持たなければなりません。 |
| 252 | + |
| 253 | +```cpp:default_arguments.cpp |
| 254 | +#include <iostream> |
| 255 | +#include <string> |
| 256 | + |
| 257 | +// 第2引数 greeting にデフォルト値を設定 |
| 258 | +void greet(const std::string& name, const std::string& greeting = "Hello") { |
| 259 | + std::cout << greeting << ", " << name << "!" << std::endl; |
| 260 | +} |
| 261 | + |
| 262 | +int main() { |
| 263 | + // 第2引数を省略。デフォルト値 "Hello" が使われる |
| 264 | + greet("Alice"); |
| 265 | + |
| 266 | + // 第2引数を指定。指定した値 "Hi" が使われる |
| 267 | + greet("Bob", "Hi"); |
| 268 | + |
| 269 | + return 0; |
| 270 | +} |
| 271 | +``` |
| 272 | +
|
| 273 | +```cpp-exec:default_arguments.cpp |
| 274 | +Hello, Alice! |
| 275 | +Hi, Bob! |
| 276 | +``` |
| 277 | + |
| 278 | +## この章のまとめ |
| 279 | + |
| 280 | +この章では、C++の関数に関する基本的ながらも重要な機能を学びました。 |
| 281 | + |
| 282 | + * **関数の宣言と定義の分離**: プログラムの構造を整理し、分割コンパイルを可能にするための基本です。 |
| 283 | + * **引数の渡し方**: |
| 284 | + * **値渡し**: 引数のコピーを作成し、元の変数を保護します。 |
| 285 | + * **参照渡し (`&`)**: 変数の「別名」を渡し、コピーのコストをなくします。呼び出し元の変数を変更するため、または効率化のために使います。 |
| 286 | + * **`const`参照渡し (`const&`)**: 効率的でありながら、意図しない変更を防ぐためのC++の定石です。 |
| 287 | + * **関数のオーバーロード**: 同じ名前で引数リストの異なる関数を定義でき、文脈に応じた適切な関数が自動で選ばれます。 |
| 288 | + * **デフォルト引数**: 関数の引数を省略可能にし、柔軟な関数呼び出しを実現します。 |
| 289 | + |
| 290 | +特に「参照」は、この先のC++プログラミングで頻繁に登場する極めて重要な概念です。値渡しとの違い、そして`const`参照との使い分けをしっかりマスターしましょう。 |
| 291 | + |
| 292 | +### 練習問題1: 値の交換 |
| 293 | + |
| 294 | +2つの`int`型変数の値を交換する関数 `swap` を作成してください。この関数は、呼び出し元の変数の値を直接変更できるように、**参照渡し**を使って実装してください。 |
| 295 | + |
| 296 | +```cpp:practice3_1.cpp |
| 297 | +#include <iostream> |
| 298 | + |
| 299 | +// ここにswap関数を実装してください |
| 300 | + |
| 301 | + |
| 302 | +// main関数 |
| 303 | +int main() { |
| 304 | + int a = 10; |
| 305 | + int b = 20; |
| 306 | + std::cout << "Before: a = " << a << ", b = " << b << std::endl; |
| 307 | + swap(a, b); |
| 308 | + std::cout << "After: a = " << a << ", b = " << b << std::endl; |
| 309 | + return 0; |
| 310 | +} |
| 311 | +``` |
| 312 | + |
| 313 | +```cpp-exec:practice3_1.cpp |
| 314 | +(期待される実行結果) |
| 315 | +Before: a = 10, b = 20 |
| 316 | +After: a = 20, b = 10 |
| 317 | +``` |
| 318 | + |
| 319 | +### 問題2: 図形の面積 |
| 320 | + |
| 321 | +**関数のオーバーロード**を使い、正方形と長方形の面積を計算する `calculate_area` という名前の関数を実装してください。 |
| 322 | + |
| 323 | +1. 引数が1つ (`int side`) の場合は、正方形の面積 (side✕side) を計算して返す。 |
| 324 | +2. 引数が2つ (`int width`, `int height`) の場合は、長方形の面積 (width✕height) を計算して返す。 |
| 325 | + |
| 326 | +作成した関数を`main`関数から呼び出し、結果が正しく表示されることを確認してください。 |
| 327 | + |
| 328 | +```cpp:practice3_2.cpp |
| 329 | +#include <iostream> |
| 330 | + |
| 331 | +``` |
| 332 | + |
| 333 | +```cpp-exec:practice3_2.cpp |
| 334 | +``` |
0 commit comments