C++ Learning

第10回 関数の書き方(オーバーロード/デフォルト引数)

C++ の関数は C の延長上にありますが、3 つの強力な拡張があります:オーバーロード(同じ名前で違う引数の関数)、デフォルト引数(省略できる引数)、const 参照渡し(重い型を速く・安全に渡す)。これで C の時代に書いていた print_int() / print_double() / print_string() の命名分けや、長すぎる引数リストから解放されます。

このページで押さえること
✅ 最低限ここだけ覚える
  • 同じ名前で引数の型や個数が違う関数が書ける(オーバーロード)
  • 引数に =既定値 を書けば省略可能(デフォルト引数)
  • 重い型を渡すときは const T&
  • 書き換えたい引数は T&
⭐ 余裕があれば読む
  • オーバーロード解決のルール
  • デフォルト引数を持てるのは「末尾の引数から」
  • trailing return type auto f() -> int
  • ヘッダで宣言/cpp で定義の書き分け

1. まず触ってみる ― 同じ名前で使い分け

C の関数との最大の違いは「同じ名前で違う関数を定義できる」こと。これをオーバーロードと呼びます。型に応じて呼び分ける仕組みで、STL の std::sortstd::max がこれで実装されています。

▶ どの関数が呼ばれる?
下の「候補となる関数」から、引数に応じてどれが選ばれるかを確認
void print(int n); void print(double x); void print(const std::string& s); void print(int n, int m);

まずは 3 つだけ

今日覚える最小の書き方:

first_func.cppC++ 最小例
// ① オーバーロード:同じ名前で違う引数 int max(int a, int b) { return a > b ? a : b; } double max(double a, double b) { return a > b ? a : b; } // ② デフォルト引数:省略可能 void greet(std::string name = "World") { std::cout << "Hello, " << name << "\n"; } // ③ const 参照渡し:コピーせず、書き換えもさせない void show(const std::string& s) { std::cout << s; }
ここまでで覚えること(3 つ):
  • 同じ名前で違う関数 → オーバーロード
  • 引数に =既定値省略可能
  • 引数を重い型で受けるときは const T&

よくある素朴な疑問

Q. C では関数名を print_int / print_double と分けていたのに、C++ では同じ名前でいいの?
→ はい。C のコンパイラは関数名で関数を識別していましたが、C++ のコンパイラは「名前 + 引数の並び」で識別します。引数が違えば別の関数として扱えるので、呼び出す側の負担が減ります。

Q. 同じ引数で「戻り値だけ違う」オーバーロードはできる?
できません。「呼び出し箇所で戻り値の型が決まらない」ので、コンパイラが区別できないからです。必ず引数の型か個数を変えてください。

Q. C で使っていた関数(strlen など)は C++ から呼べる?
→ 呼べます。#include <cstring>(C の string.h 相当)で使えます。

2. 関数の基本(C との差分)

関数の基本構文は C と同じですが、微妙な違いがいくつかあります。

CC
// 引数なしは void を明示 int hello(void); // プロトタイプ宣言が必要 int add(int, int); int main(void) { return add(1, 2); } int add(int a, int b) { return a + b; }
C++C++
// 引数なしは () でよい(void 省略) int hello(); // main 末尾の return 0; も省略可(非推奨) int main() { return add(1, 2); // 前方宣言がなくてもエラーになる点は同じ } int add(int a, int b) { return a + b; }

引数なしと void

C では int f() と書くと「引数の情報なし(任意個の引数を受け取れる、という意味)」でした。だから int f(void) と明示していました。C++ では int f()「引数なし」を明示する書き方です。void は書いても書かなくても OK で、現代の C++ では省略するのが一般的。

プロトタイプ宣言は C と同じ

関数を呼び出す位置よりに定義を書く場合は、前もって宣言(プロトタイプ)が必要です。C と同じ。ヘッダファイルに宣言、.cpp に定義、という分離は C++ でも基本です(詳しくは「複数ファイル分割」の回で)。

トレイリング戻り型 -> 型(C++11)

C++11 から、戻り値の型を関数名の後ろに書く記法が使えます:

後置戻り型C++11~
// 従来 int add(int a, int b) { return a + b; } // 後置(意味は同じ) auto add(int a, int b) -> int { return a + b; }

普段は従来の書き方で OK。戻り値の型が引数に依存する複雑なテンプレートで威力を発揮します(この段階では気にしなくて大丈夫)。

3. 関数オーバーロード

関数オーバーロードは C++ の看板機能のひとつ。C の abs / fabs / labs のような型ごとの命名分けを不要にします。

C(型ごとに名前を変える)煩雑
int abs(int x); // <stdlib.h> long labs(long x); // <stdlib.h> double fabs(double x); // <math.h> double y = fabs(-3.14); int z = abs(-5);
C++(同じ名前)シンプル
#include <cmath> // std::abs は int, long, double すべてにオーバーロード済み double y = std::abs(-3.14); int z = std::abs(-5);

オーバーロードできる条件

次のいずれかが違えばオーバーロードとして区別されます:

逆に、戻り値の型だけが違うオーバーロードは作れません:

NGコンパイルエラー
int f(int x); double f(int x); // ← エラー: 戻り値だけ違うのは NG

理由:f(42) のような呼び出しで、呼ぶ側がどちらを選ぶのか決められないからです。

オーバーロードの選ばれ方(解決)

コンパイラは次の順に「一番適合する関数」を探します:

  1. 完全一致(引数の型が全く同じ)
  2. 軽い変換(int→long、配列→ポインタ、など)
  3. 標準変換(int→double、派生クラス→基底クラス参照、など)
  4. それでも決まらなければ曖昧としてエラー
曖昧エラーの例: void f(int, double);void f(double, int); がある状態で f(1, 1);(両方とも int)とすると、どちらも「片方は完全一致、もう片方は変換」になり優劣が付かずコンパイルエラー。普段は深く考えなくて済みますが、「曖昧です」と言われたらこのルールを思い出してください。

4. デフォルト引数

引数に = 既定値 を書いておくと、呼び出し側で省略できます。

C では別関数が必要冗長
void greet(void) { puts("Hello, World!"); } void greet_name(const char* n) { printf("Hello, %s!\n", n); } greet(); greet_name("Alice");
C++ ならこれだけ1 関数
void greet(std::string name = "World") { std::cout << "Hello, " << name << "!\n"; } greet(); // "Hello, World!" greet("Alice"); // "Hello, Alice!"

ルール:末尾から連続して指定する

デフォルト引数は右(末尾)から指定します。途中にデフォルトを付けて、後ろに付けない、ということはできません。

OK末尾から
void f(int a, int b = 0, int c = 0); f(1); // a=1, b=0, c=0 f(1, 2); // a=1, b=2, c=0 f(1, 2, 3); // a=1, b=2, c=3
NGエラー
void f(int a = 0, int b, int c); // ← NG: 途中だけデフォルト void g(int a, int b = 0, int c); // ← NG: デフォルトの後ろに非デフォルト
オーバーロードとの競合に注意: デフォルト引数を持つ関数と、同名のオーバーロードを両方作ると「どちらを呼ぶのか曖昧」になりエラー。たとえば void f(int a, int b = 0);void f(int a); が両方あると、f(1); でどちらを呼ぶか決められません。どちらか片方にするのが無難。

宣言と定義でデフォルトを書く場所

ヘッダで宣言して .cpp で定義するとき、デフォルト値は宣言(ヘッダ)側にだけ書きます。両方に書くとエラー。

ここまでで関数の基本は OK
残り 2 節は「引数の渡し方」と「戻り値」の話。std::string や自作クラスを扱い始めると重要になる章です。

5. 引数の渡し方 ― 値 / 参照 / const 参照

C では「値渡し」か「ポインタ渡し」の 2 択でしたが、C++ には参照が加わって 3 択になります。使い分けを覚えると、コードが格段に速く・安全になります。

① 値渡し void f(T x)
📋
引数のコピーが渡る。関数内で書き換えても呼び出し元には影響しない。小さい型(int, double など)ではこれで OK。
② 参照渡し void f(T& x)
✏️
引数への参照が渡る。関数内の変更は呼び出し元にも反映される。書き換えたいときに使う(swap、出力引数など)。
③ const 参照渡し void f(const T& x)
🔒
参照で受け取るが、変更禁止。コピーしないので速い、かつ書き換えの心配もない。重い型を読むだけのときの既定。

判断フロー

迷ったときの判断(これだけ覚えれば OK):
  • 引数の型が int / double / bool など小さい値渡し
  • 関数内で書き換えたい参照 T&
  • 関数内で読むだけ、かつ型が重いstringvector、クラスなど)→ const 参照 const T&

具体例

全部乗せ使い分け
// ① 小さい型は値渡し int square(int x) { return x * x; } // ② 書き換えたいなら参照 void reset(int& x) { x = 0; } // ③ 重い型を読むだけなら const 参照 void print(const std::string& s) { std::cout << s; } // ④ 重い型を書き換えるなら 参照 void append(std::string& s, const std::string& t) { s += t; }
NG 例遅い/危険
// NG: 重い型を値で渡す(毎回コピー) void print(std::string s) { std::cout << s; } // NG: 意図せず書き換え可能 void show(std::string& s) { std::cout << s; // s = ""; ← うっかり書き換えてもコンパイラは止めない }
参照 T& と ポインタ T* の違い: どちらも「元の変数を指す」点は同じですが、参照は必ず何かを指すnullptr 相当がない、途中で指す先を変えられない)ので安全です。関数の引数で「必ず 1 つのオブジェクトを渡す」なら参照、「nullptr も受け入れる/配列の先頭を渡す」ならポインタ、と使い分けます。詳しくは第 8 回「参照」で。

6. 戻り値のあれこれ

基本:値で返す

戻り値は基本値で返す(コピーが起きるのでは?と心配するかもしれませんが、現代のコンパイラは RVO / NRVO という最適化でほぼコピーを消します。詳しくは「ムーブ」の回で)。

値で返す既定
std::string greet() { return "Hello"; // RVO でコピーしない } std::vector<int> make_data(int n) { std::vector<int> v(n); // 中身を埋める... return v; }

参照で返す(注意が必要)

関数のローカル変数への参照を返してはいけません。ローカル変数はスコープを抜けると壊れるため、ダングリング参照になります。

NGダングリング
int& bad() { int x = 42; return x; // ← スコープを抜けると x は消える } int& y = bad(); std::cout << y; // 未定義動作
OKメンバ/引数への参照
class Foo { int data_; public: int& data() { return data_; } // OK: メンバへの参照 }; // 引数の参照を返すのも OK int& at(std::vector<int>& v, int i) { return v[i]; }

複数の値を返したい

C では「ポインタ引数で出力」か「構造体にまとめて返す」しか手がありませんでした。C++ では選択肢が増えます:

例:割り算の商と余りを返す
auto div_mod(int a, int b) { return std::pair{a / b, a % b}; // {商, 余り} } auto [q, r] = div_mod(17, 5); // q=3, r=2
広告スペース

確認クイズ

関数・オーバーロード・引数渡しの理解度を 4 問で確認しましょう。

Q1. 次のうちオーバーロードとして成立するペアはどれ?

int f(int); と int f(int);
int f(int); と double f(double);
int f(int); と double f(int);
int f(int a); と int f(int b);
①は完全に同一。③は戻り値だけ違う(NG)。④は仮引数名が違うだけで型は同じ(NG)。②は引数の型が違うので別関数として区別でき、オーバーロード成立。

Q2. void f(int a, int b = 0, int c); はコンパイルできる?

できる
できない(デフォルト引数のあとに非デフォルトがある)
警告は出るがコンパイルは通る
できるが c は自動的に 0 になる
デフォルト引数は右(末尾)から連続して指定するルール。void f(int a, int b, int c = 0); なら OK。f(1, 2); の呼び出しで「b を省略したのか c を省略したのか」が決められないため、このルールがあります。

Q3. std::string を「読むだけ」で関数に渡す、最も良い書き方は?

void f(std::string s)
void f(std::string& s)
void f(std::string* s)
void f(const std::string& s)
const T&コピーしない(速い)かつ書き換えできない(安全)の両立。①は毎回コピーで遅い。②は意図せず書き換えられるかも。③はポインタで nullptr の心配があり冗長。

Q4. 次のコードの出力は?
void f(int x) { x = 99; }
int main(){ int a = 1; f(a); std::cout << a; }

99
1
0
コンパイルエラー
f(int x)値渡しなので、xa のコピー。x を書き換えても a は影響を受けない。書き換えたいなら void f(int& x) にします(その場合は 99)。
この記事をシェア