前回と前々回でC++の基本的なデータ型と制御構造について一通り説明しました。今回は「関数 (function)」の基本的な使い方について説明します。
プログラミングは模型を組み立てる作業と似ています。簡単な処理はC++の機能やライブラリを使って実現することができます。ところが、模型が大きくなると、一度に全体を組み立てるのは難しくなります。このような場合、全体をいくつかに分割して、まずその部分ごとに作ります。最後に、それを結合して全体を完成させます。
これはプログラミングにも当てはまります。実現しようとする処理が複雑になると、一度に全部作ることは難しくなります。そこで、全体を小さな処理に分割して、一つ一つの処理を作成し、それらを組み合わせて全体のプログラムを完成させます [*1]。
分割した処理を作成する場合、それを一つの部品として扱えると便利です。つまり、小さな部品を作り、それを使って大きな部品を作り、最後にそれらを組み合わせて全体を完成させます。このとき、もっとも基本となる部品が「関数」[*2] です。
C++の関数定義はとても簡単です。例題として、数を二乗する関数を作ってみましょう。次のリストを見てください。
リスト : 数を二乗する関数 (sample30.cpp) #include <iostream> using namespace std; int square(int x) { return x * x; } int main() { cout << square(10) << endl; }
関数定義の構文を下図に示します。
データ型 名前(仮引数名, ...) { 処理A; 処理B; ... } 図 : C++の関数定義
関数の定義は下図のように数式と比較するとわかりやすいでしょう。
f (x) = x * x 型 名前 引数 処理内容 int square (int x) { return x * x; } 図 : 関数定義と数式の比較
square が関数名、( ) の中の x が入力データを受け取る引数、ブロック { } の中の return x * x が実行される処理です。関数定義で使用する引数のことを「仮引数」、実際に与えられる引数を「実引数」といいます。square の定義で使用した x が仮引数で、square(10) の 10 が実引数になります。仮引数は変数と同じ方法で宣言します。データ型のあとに名前を指定します。
そして、関数が出力する値を「返り値」といいます。返り値のデータ型は関数名の前で指定します。C/C++の場合、関数の値は return 文を使って返します。return x * x; とすることで、x * x の計算結果を返します。
それでは実際に実行してみましょう。
$ clang++ sample30.cpp $ ./a.out 100
なお、値を返さない関数も定義することができます。この場合、返り値のデータ型には void を指定してください。
それでは、ここで変数 x に値が代入されている場合を考えてみましょう。次の例を見てください。
リスト : 局所変数と大域変数 (sample31.cpp) #include <iostream> using namespace std; int x = 10; int square(int x) { return x * x; } int main() { cout << x << endl; cout << square(5) << endl; cout << x << endl; }
$ clang++ sample31.cpp $ ./a.out 10 25 10
関数の外側で変数 x を定義しています。これを「グローバル変数 (golbal variable)」もしくは「大域変数」といいます。関数 main の中で、最初の出力は大域変数 x を参照するので 10 が表示されます。
それでは、square(5) の実行結果はどうなると思いますか。x には 10 がセットされているので 10 の二乗を計算して返り値は 100 になるのでしょうか。これは 5 の二乗を計算して結果は 25 になります。そして、square を実行したあとでも大域変数 x の値は変わりません。
square の仮引数 x は、その関数が実行されている間だけ有効です。このような変数を「ローカル変数 (local variable)」とか「局所変数」といいます。これに対し、大域変数は同一ファイル内であればプログラムのどこからでも参照することができます。C/C++は変数の値を求めるとき、それが局所変数であればその値を参照します。局所変数でなければ大域変数の値を参照します。
プログラムを作る場合、関数を部品のように使います。ある関数を呼び出す場合、今まで使っていた変数の値が勝手に書き換えられると、呼び出す方が困ってしまいます。部品であるならば、他の処理に影響を及ぼさないように、自分自身の中で処理を完結させることが望ましいのです。これを実現するための必須機能が局所変数なのです。
C/C++の場合、関数の仮引数は局所変数になりますが、それ以外にも関数の中で局所変数が必要になる場合があります。C/C++は関数内で宣言された変数を局所変数として扱います。今まで関数 main の中で変数を宣言しましたが、これらの変数はすべて局所変数になります。
簡単な例を示しましょう。
リスト : 局所変数の有効範囲 (sample32.cpp) #include <iostream> using namespace std; int main() { int x = 1; { int y = 2; { int z = 3; cout << x << endl; cout << y << endl; cout << z << endl; } cout << x << endl; cout << y << endl; // cout << z << endl; z は範囲外 (コンパイルエラー) } cout << x << endl; // cout << y << endl; y は範囲外 (コンパイルエラー) // cout << z << endl; z は範囲外 (コンパイルエラー) }
$ clang++ sample32.cpp $ ./a.out 1 2 3 1 2 1
局所変数が値を保持する期間のことを、変数の「有効範囲 (scope : スコープ)」といいます。C/C++の場合、局所変数の有効範囲は変数が定義されているブロックの中だけです。for 文の場合も同じで、初期化処理で宣言された局所変数は、そのあとのブロックが有効範囲になります。
変数 x は関数 main の一番外側のブロックで定義されているので、main の処理が終了するまで有効です。変数 y は 2 番目のブロックで、変数 z は 3 番目のブロックで定義されているので、各々のブロックの終わりまでが変数の有効範囲になります。ブロックの実行が終了すると、そのブロックで定義された局所変数は廃棄されます。
したがって、3 番目のブロックの中では変数 x, y, z が有効です。ブロックの処理が終了すると変数 z が廃棄されるので、2 番目のブロックの中では変数 x, y が有効です。そして、そのブロックの処理が終了すると、変数 y が廃棄されるので有効な変数は x だけになります。
関数の仮引数は局所変数と同じと考えてください。関数を呼び出すとき、実引数を仮引数に代入してから、関数本体の処理を実行します。関数の実行が終了するとき、仮引数は廃棄されます。
C++の関数は引数にデフォルトの値を設定することができます。これを「デフォルト引数」といます。値は = で指定します。簡単な例を示しましょう。
リスト : デフォルト引数 (sample33.cpp) #include <iostream> using namespace std; void foo(int a, int b = 10, int c = 100) { cout << a << " " << b << " " << c << endl; } int main() { foo(1); foo(1, 2); foo(1, 2, 3); }
$ clang++ sample33.cpp $ ./a.out 1 10 100 1 2 100 1 2 3
関数 foo の引数 a は通常の引数で、引数 b と c がデフォルト値を指定した引数です。デフォルト引数は通常の引数の後ろに定義します。foo を呼び出すとき、引数 a の値を渡さないといけませんが、引数 b と c の値は省略することができます。このとき、使用される値がデフォルト値です。
たとえば、foo(1) と呼び出すと 1 10 100 と表示され、引数 b と c の値はデフォルト値が使用されていることがわかります。foo(1, 2) と呼び出すと、引数 b の値はデフォルト値ではなく、実引数 2 が b の値になります。同様に、foo(1, 2, 3) と呼び出すと、仮引数 c の値は実引数 3 になるので 1 2 3 と表示されます。
C++は同じ名前空間の中で同名の関数を複数定義することができます。これを関数の「多重定義 (overload)」といいます。ただし、引数のデータ型、個数、並び方などが異なる必要があります。これらがまったく同じ関数を多重定義することはできません。
簡単な例を示しましょう。
リスト : 関数の多重定義 (sample34.cpp) #include <iostream> using namespace std; int max(int a, int b) { return a >= b ? a : b; } double max(double a, double b) { return a >= b ? a : b; } int main() { cout << max(1, 2) << endl; cout << max(1.1, 2.2) << endl; }
$ clang++ sample34.cpp $ ./a.out 2 2.2
関数 max は引数 a と b で大きいほうを返します。同じ名前の関数が 2 つ定義されていますが、仮引数のデータ型が異なっています。実引数に int を渡すと int max(int a, int b) が呼び出されます。double を渡すと double max(double a, double b) が呼び出されます。
このように、C++では同じ名前の関数を複数定義することができます。なお、関数 max はC++の標準ライブラリ <algorithm> に定義されているので、私たちが自分で定義する必要はありません。
ところで、C++は演算子も多重定義することができます。出力演算子 << は整数、浮動小数点数、文字列などいろいろなデータを出力できますが、これは << が多重定義されているからできることなのです。演算子の多重定義は回を改めて説明する予定です。
関数を多重定義するとき、曖昧さが生じないように注意してください。簡単な例を示しましょう。
リスト : 曖昧な多重定義 (sample35.cpp) #include <iostream> using namespace std; void foo(int a) { cout << a << endl; } void foo(int a, int b = 10, int c = 100) { cout << a << " " << b << " " << c << endl; } int main() { foo(1); foo(1, 2); foo(1, 2, 3); }
関数 foo が多重定義されていますが、foo(1) はどちらの関数が呼び出されるのでしょうか。実は、どちらの関数も呼び出すことが可能で、コンパイラが判断できずにエラーとなります。
$ clang++ sample35.cpp sample35.cpp:16:3: error: call to 'foo' is ambiguous foo(1); ^~~ sample35.cpp:4:6: note: candidate function void foo(int a) ^ sample35.cpp:9:6: note: candidate function void foo(int a, int b = 10, int c = 100) ^ 1 error generated.
このプログラムでは、2 番目の定義で引数 b のデフォルト値を削除すると、正常にコンパイルすることができます。つまり、引数がひとつのときは最初の定義が呼び出され、ふたつ以上の場合は 2 番目の定義が呼び出されます。
リスト : 曖昧な多重定義 (修正版) #include <iostream> using namespace std; void foo(int a) { cout << a << endl; } void foo(int a, int b, int c = 100) { cout << a << " " << b << " " << c << endl; } int main() { foo(1); foo(1, 2); foo(1, 2, 3); }
$ clang++ sample35.cpp $ ./a.out 1 1 2 100 1 2 3
また、次に示すようにデータ型の型変換が行われるとき、多重定義に曖昧さが生じる場合があります。
リスト : 曖昧な多重定義 (sample36.cpp) #include <iostream> using namespace std; void foo(float x) { cout << "float: " << x << endl; } void foo(double x) { cout << "double: " << x << endl; } int main() { foo(123); }
$ clang++ sample36.cpp sample36.cpp:16:3: error: call to 'foo' is ambiguous foo(123); ^~~ sample36.cpp:4:6: note: candidate function void foo(float x) ^ sample36.cpp:9:6: note: candidate function void foo(double x) ^ 1 error generated.
関数 foo は、引数の型が float と double の 2 種類あります。foo(123) のように整数型を渡して呼び出す場合、C++は整数を浮動小数点数に型変換して呼び出しますが、float と double のどちらの型に変換してよいか判断できず、コンパイルエラーとなります。
この場合、引数が int 型の関数 foo を定義すると、曖昧さを取り除くことができます。
リスト : 曖昧な多重定義 (修正版) #include <iostream> using namespace std; void foo(float x) { cout << "float: " << x << endl; } void foo(double x) { cout << "double: " << x << endl; } void foo(int x) { cout << "int: " << x << endl; } int main() { foo(123); foo(1.2345); foo(1.2345f); }
$ clang++ sample36.cpp $ ./a.out int: 123 double: 1.2345 float: 1.2345
それでは簡単な例題として、整数 n から m までの和、二乗の和、三乗の和を求める関数 sum_of_int, sum_of_square, sum_of_cube を作ってみましょう。なお、int ではすぐにオーバーフローするので long long int を使って計算することにします。
繰り返しでプログラムを作ると、次のようになるでしょう。
リスト : 整数の和, 二乗の和, 三乗の和 (sample37.cpp) #include <iostream> using namespace std; long long sum_of_int(int n, int m) { long long sum = 0; for (; n <= m; n++) sum += n; return sum; } long long square(long long x) { return x * x; } long long sum_of_square(int n, int m) { long long sum = 0; for (; n <= m; n++) sum += square(n); return sum; } long long cube(long long x) { return x * x * x; } long long sum_of_cube(int n, int m) { long long sum = 0; for (; n <= m; n++) sum += cube(n); return sum; } int main() { cout << sum_of_int(1,10000) << endl; cout << sum_of_int(100, 10000) << endl; cout << sum_of_square(1, 10000) << endl; cout << sum_of_square(100, 10000) << endl; cout << sum_of_cube(1, 10000) << endl; cout << sum_of_cube(100, 10000) << endl; }
$ clang++ sample37.cpp $ ./a.out 50005000 50000050 333383335000 333383006650 2500500025000000 2500500000497500
sum_of_xxx の引数 n, m は int で、返り値が long long になります。どの関数も局所変数 sum に合計値を求めます。関数 square は引数 x を二乗し、関数 cube は引数 x を三乗します。引数の型を long long int で定義しているので、関数を呼び出すとき実引数は int から long long int に変換されます。あとはとくに難しいところはないでしょう。
ところで、整数の和は次の公式を使ってもっと簡単に求めることができます。
これをプログラムすると次のようになります。
リスト : 整数の和、二乗の和、三乗の和 (sample38.cpp) #include <iostream> using namespace std; long long sum_of_int(long long n) { return n * (n + 1) / 2; } long long sum_of_int(int n, int m) { return sum_of_int(m) - sum_of_int(n - 1); } long long sum_of_square(long long n) { return n * (n + 1) * (2 * n + 1) / 6; } long long sum_of_square(int n, int m) { return sum_of_square(m) - sum_of_square(n - 1); } long long sum_of_cube(long long n) { return n * n * (n + 1) * (n + 1) / 4; } long long sum_of_cube(int n, int m) { return sum_of_cube(m) - sum_of_cube(n - 1); } int main() { cout << sum_of_int(1,10000) << endl; cout << sum_of_int(100, 10000) << endl; cout << sum_of_square(1, 10000) << endl; cout << sum_of_square(100, 10000) << endl; cout << sum_of_cube(1, 10000) << endl; cout << sum_of_cube(100, 10000) << endl; }
1 引数の関数 sum_of_xxx で 1 から n までの合計値を求めます。あとは 2 引数の関数 sum_of_xxx で、sum_of_xxx(m) - sum_of_xxx(n - 1) を計算するだけです。このように、引数の個数が異なれば、多重定義により同名の関数を定義することができます。
次は累乗を求める関数 power を作ってみましょう。累乗は x の y 乗という x を y 回掛ける計算です。累乗は x の右上に小さく y を書くことで表されますが、ここでは x ** y と書くことにします。
power (x, y) = x ** y x ** 3 = x * x * x; x ** 4 = x * x * x * x; x ** 5 = x * x * x * x * x; 図 : 累乗の計算
今回のプログラムは、引数 x を実数、y を整数とします。単純な繰り返しでプログラムを作ると次のようになります。
リスト : 累乗 (sample39.cpp) #include <iostream> using namespace std; double power(double x, int y) { double z = 1; while (y-- > 0) z *= x; return z; } int main() { for (int i = 0; i < 64; i += 4) { cout << power(2, i) << endl; } }
$ clang++ sample39.cpp $ ./a.out 1 16 256 4096 65536 1.04858e+06 1.67772e+07 2.68435e+08 4.29497e+09 6.87195e+10 1.09951e+12 1.75922e+13 2.81475e+14 4.5036e+15 7.20576e+16 1.15292e+18
この場合、y 回の乗算が必要になります。ところが、式を変形するともっと少ない回数で求めることができます。整数 y は 2 ** i の和の形で表せることを利用します。次の例を見てください。
11 = 1 + 2 + 8 (2**0 + 2**1 + 2**3) 15 = 1 + 2 + 4 + 8 (2**0 + 2**1 + 2**2 + 2**3)
これは整数 y の右側 (LSB) から順番にビットを拾っていけば簡単に求めることができます。
MSB LSB bit ... 7 6 5 4 3 2 1 0 | | | | | | | | | | | | | | | +---> 2**0 : 1 | | | | | | +---> 2**1 : 2 | | | | | +---> 2**2 : 4 | | | | +---> 2**3 : 8 | | | +---> 2**4 : 16 | | +---> 2**5 : 32 | +---> 2**6 : 64 +---> 2**7 : 128
これを利用すると x ** n は次のように求めることができます。
x**11 = x * x**2 * x**8 x**15 = x * x**2 * x**4 * x**8 x**2 = x * x x**4 = x**2 * x**2 x**8 = x**4 * x**4 .....
x**2, x**4, x**8, ... は x を次々に自乗していけば簡単に求めることができます。これをプログラムすると次のようになります。
リスト : 累乗 (2) double power_f(double x, int y) { double z = 1; for (; y > 0; y /= 2) { if (y % 2 != 0) z *= x; x *= x; } return z; }
for ループの中で、x を自乗して y を 1 / 2 にします。これで、x の値は x, x**2, x**4, x**8, x**16 と増えていきます。そして、y が奇数 (最下位のビットが 1) ならば、z に x を乗算します。最後に、y が 0 になったらループを脱出し、return で z を返します。これで高速に累乗を求めることができます。
ところで、C++の標準ライブラリ <cmath> には累乗を求める関数 pow が用意されています。
double pow(double d, double e); double pow(double d, int i); // C言語の標準ライブラリには無い
このほかにも、<cmath> には標準的な数学関数が用意されています。
それでは、実際に関数 pow を使ってみましょう。
リスト : 関数 pow の使用例 (sample3a.cpp) #include <iostream> #include <cmath> using namespace std; int main() { for (int i = 0; i < 64; i += 4) { cout << pow(2, i) << endl; } }
プログラムは clang++ sample3a.cpp でコンパイルすることができます。C言語と違って、ライブラリを指定するオプション -lm は必要ありません。
今度は、前回作成した素数を求めるプログラムを関数を使って書き直してみましょう。次のリストを見てください。
リスト : 素数を求める (sample3b.cpp) #include <iostream> using namespace std; const int N = 100; // 素数を格納する配列 int prime_table[N]; int prime_size; // 初期化 void init_prime(void) { prime_table[0] = 2; prime_size = 1; } // 素数か? bool is_prime(int n) { for (int i = 0; i < prime_size; i++) { int p = prime_table[i]; if(p * p > n) break; if(n % p == 0) return false; } return true; } // 素数を追加する void push_prime(int n) { prime_table[prime_size++] = n; } // 素数の表示 void print_prime() { for (int i = 0; i < prime_size; i++) cout << prime_table[i] << " "; cout << endl; } int main() { // 初期化 init_prime(); // 素数を求める for (int n = 3; n <= N; n += 2) if (is_prime(n)) push_prime(n); // 表示 print_prime(); }
$ clang++ sample3b.cpp $ ./a.out 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
数値 n が素数か判定する処理を関数 is_prime で行うように変更します。is_prime は数値 n を受け取り、n が素数で割り切れれば false を返し、そうでなければ true を返します。for ループの中で return false を実行すれば、is_prime の処理を終了して false を返すことができます。
is_prime を使うと、素数を求める処理は簡単にプログラムすることができます。is_prime が true を返したら n を配列 prime_table に追加するだけです。この処理も関数 push_prime で行うように修正しています。同様に、prime_table の初期化と素数の表示も関数にしています。このように、処理を分割して関数に割り当てることにより、関数 main はとてもわかりやすいプログラムになります。
実数 a の平方根 \(\sqrt a\) の値を求める場合、方程式 \(x^2 - a = 0\) を Newton (ニュートン) 法で解くことが多いと思います。方程式を \(f(x)\), その導関数を \(f'(x)\) とすると、ニュートン法は次の漸化式の値が収束するまで繰り返す方法です。
平方根を求める場合、導関数は \(f'(x) = 2x\) になるので、漸化式は次のようになります。
参考文献『C言語による最新アルゴリズム事典』によると、\(\sqrt a\) より大きめの初期値から出発し、x = (x + a / x) / 2 を減少が止まるまで繰り返すことで \(\sqrt a\) の正確な値を求めることができるそうです。
これをC++でプログラムすると、次のようになります。
リスト : 平方根を求める (sample3c.cpp) #include <iostream> #include <iomanip> #include <cmath> using namespace std; double mysqrt(double x) { if (x > 0) { double p = x > 1 ? x : 1; double q; do { q = p; p = (p + x / p) / 2; } while (p < q); return q; } return 0; } int main() { cout << setprecision(16); for (int i = 2; i < 10; i++) { cout << sqrt(i) << endl; cout << mysqrt(i) << endl; } }
アルゴリズムをそのままプログラムしただけなので、とくに難しいところは無いと思います。実行結果は次のようになります。標準ライブラリ <cmath> の関数 sqrt と比較してみました。
$ clang++ sample3c.cpp $ ./a.out 1.414213562373095 1.414213562373095 1.732050807568877 1.732050807568877 2 2 2.23606797749979 2.23606797749979 2.449489742783178 2.449489742783178 2.645751311064591 2.645751311064591 2.82842712474619 2.82842712474619 3 3
正常に動作していますね。
整数 n の平方根の整数部分を求めることも簡単です。次のリストを見てください。
リスト : 整数 n の平方根を求める (sample3d.cpp) #include <iostream> using namespace std; int mysqrt(int x) { if (x > 0) { int p = x; int q; do { q = p; p = (p + x / p) / 2; } while (p < q); return q; } return 0; } int main() { cout << mysqrt(100) << endl; cout << mysqrt(1000) << endl; cout << mysqrt(10000) << endl; cout << mysqrt(123456789) << endl; cout << mysqrt(1234567890) << endl; }
double で宣言していた引数や変数を int に変更しただけです。実行結果は次のようになります。
$ clang++ sample3d.cpp $ ./a.out 10 31 100 11111 35136
これも正常に動作しています。
もうひとつ、平方根の整数部分を求める簡単な方法を紹介しましょう。平方根の整数部分は次の公式を使って求めることができます。
式 (1) は、奇数 \(1\) から \(2n - 1\) の総和は \(n^2\) になることを表しています。式 (2) のように、整数 m の値が \(n^2\) より大きくて \((n + 1)^2\) より小さいのであれば、m の平方根の整数部分は n であることがわかります。これは m から奇数 \(1, 3, 5, \ldots, (2n - 1), (2n + 1)\) を順番に引き算していき、引き算できなくなった時点の (2n + 1) / 2 = n が m の平方根になります。参考文献『平方根計算法』によると、この方法を「めのこ平方」と呼ぶそうです。
プログラムは次のようになります。
リスト : めのこ平方 (sample3e.cpp.c) #include <iostream> using namespace std; int sqrt_int(int n) { int m = 1; while (n >= m) { n -= m; m += 2; } return m / 2; } int main() { cout << sqrt_int(100) << endl; cout << sqrt_int(1000) << endl; cout << sqrt_int(10000) << endl; cout << sqrt_int(123456789) << endl; }
$ clang++ sample3e.cpp $ ./a.out 10 31 100 11111
アルゴリズムをそのままプログラムしただけなので、とくに難しいところは無いと思います。この方法はとても簡単ですが、数が大きくなると時間がかかるようになります。この改良は次回の楽しみに取っておきましょう。
今回はここまでです。次回は「再帰定義」について詳しく説明します。