C++ における関数:明確で再利用可能なコードを書くための重要な要素
プログラムが大きくなるにつれて、そのコードが理解しにくく、保守しにくくなることに気づいたことはありますか。もし自分のコードが迷路のように複雑に見えると感じたことがあるなら、それは C++ における関数を十分に活用していないからです。関数はプログラムを管理しやすい部分に分割する構成要素として機能し、読みやすさ、保守性、最適化を容易にします。本講義では、関数を効果的に利用してコードの構造化を改善し、より体系的なプログラムを書き、C++ における開発をより専門的かつ効率的にする方法を学びます。
学習目標
この講義を終えると、次のことが学べます。
- 理解する:関数の目的と、それが C++ において不可欠である理由。
- 作成する:正しく関数を定義し、構造化されたコードを実現する。
- 呼び出す:プログラム内で関数を呼び出し、その実行方法を理解する。
- 区別する:値を返す関数と、単に命令を実行する関数を区別する。
- 比較する:関数を定義するさまざまな方法を比較し、状況に応じて最適なものを選ぶ。
目次
関数の宣言、呼び出し、定義
アプローチ:宣言 – 呼び出し – 定義
アプローチ:呼び出し前に宣言と実装を行う
戻り値の伝播
再帰:自分自身を呼び出す関数
関数における複数の戻り値
関数のオーバーロード (overloading)
C++ におけるインライン関数
C++ における関数に関する最終的な考察
関数の宣言、呼び出し、定義
C++ において、関数はプログラムをモジュール的かつ組織的に構築できる再利用可能なコードのブロックです。各関数は特定のタスクをカプセル化しており、コードの明確さと保守性を向上させます。プログラム内で関数を利用するためには、宣言、呼び出し、定義 の三つの基本的なステップを踏む必要があります。
これら三つの概念は不可欠であり、それぞれがコードの構造において特有の役割を果たします。以下で詳細に見ていきましょう。
- 関数の宣言
関数をコード内で使用する前に、コンパイラにその存在を知らせる必要があります。これは関数の宣言またはプロトタイプによって行われます。
関数の宣言はコンパイラに次の三つの基本事項を伝えます。
- 関数が返すデータ型(何も返さない場合は void)。
- 関数の名前。
- 受け取るパラメータ(ある場合)、およびそれらの型。
関数宣言の一般的な構文は次のとおりです。
tipo_de_retorno nombre_de_funcion (lista_de_parametros);
関数の宣言は通常、
main()の前、または複数のファイルで作業する場合はヘッダファイル .h に置かれます。 - 関数の呼び出し
関数を宣言した後、それを呼び出すことでコード内で実行できます。
関数を呼び出すとき:
- その定義に含まれるコードが実行されます。
- 関数が値を返す場合、その値は変数に格納するか、式の中で直接利用できます。
- 関数が
void型の場合、単に命令を実行し、何も返しません。
関数を呼び出す際の構文は、その名前を書き、必要ならば引数を括弧内に渡すだけです。
nombre_de_funcion(argumentos);
- 関数の定義
最後に、関数の定義はその動作を実装する部分です。ここでは、関数が呼び出されたときに実行される命令が指定されます。
関数定義の一般的な構文は次のとおりです。
tipo_de_retorno nombre_de_funcion (lista_de_parametros) { // 関数本体: 実行される命令 return valor; // (関数が値を返す場合) }各関数定義は次のルールに従う必要があります。
- (事前に宣言されている場合)宣言と一致しなければなりません。
- 関数が値を返す場合(例:
int)、返す値を伴うreturn文を含める必要があります。 - 関数が何も返さない場合(
void)、命令を実行するだけでreturnは不要です。
実行の流れ
プログラムが実行されると、関数は main() に現れる順序で呼び出されます。実行の流れは次のとおりです。
- コンパイラが関数の宣言を認識する。
main()内で関数呼び出しに到達すると、プログラムの制御がその関数の定義に移る。- 関数がその命令を実行する。
- 関数に戻り値がある場合、それが呼び出し元の行に返される。
- プログラムの流れが
main()または呼び出しを行った関数に戻る。
事前宣言の重要性
関数を使用する前に宣言することは非常に重要です。これは、C++ のコンパイラがコードを上から下へと処理するためです。関数が定義または宣言される前に呼び出そうとすると、エラーが発生します。
これに対処する主な方法は二つあります。
- 関数を
main()の前に宣言し、その後に定義する(これまでに見た方法)。 main()の前に関数を定義し、宣言を不要にする。
どちらの方法も有効ですが、前者は関数が複数のファイルに分かれている大規模なプログラムで特に有用です。
アプローチ:宣言 – 呼び出し – 定義
C++ における関数を構築するために最もよく用いられるアプローチの一つが、宣言 – 呼び出し – 定義 です。この方法に従うことで、コードを三つの基本的な段階に整理することができます。
- 宣言: 使用前に関数の存在をコンパイラに知らせ、その名前、戻り値の型、パラメータ(ある場合)を指定します。
- 呼び出し: 関数はメインコード(多くの場合
main())内で呼び出され、その内容を実行します。 - 定義: 関数の実装を詳細に示し、呼び出されたときにどの命令を実行するかを指定します。
この構造によりコードの整理が容易になり、保守性と拡張性が向上します。次に、このアプローチを適用した例を見てみましょう。
例: 関数 consoladice()
以下のコードでは、宣言 – 呼び出し – 定義 の順序に従っています。
#include <iostream>
using namespace std;
// まず関数を宣言
void consoladice();
int main() {
// 関数を呼び出す
consoladice();
return 0;
}
// 先に宣言した関数を定義し、その内部の動作を記述
void consoladice() {
cout << "Esto es una simple cadena de caracteres o literales." << endl;
cout << "Ahora te muestro el numero cinco. Aqui esta: " << 5 << endl;
cout << "Veamos que resultado nos da si hacemos 10/5. El resultado es: " << 10/5 << endl;
cout << "Una forma tipica de aproximar el numero Pi es haciendo 22/7. El resultado es: " << 22/7 << endl;
cout << "En C++ no es lo mismo escribir 22/7 que 22.0/7, el trato es diferente." << endl;
cout << "Con este simple cambio podemos ver que 22.0/7 es igual a " << 22.0/7 << endl;
cout << "No te parece esto una mejor aproximacion?" << endl;
}
このコードの期待される結果は次のとおりです。
Esto es una simple cadena de caracteres o literales.
Ahora te muestro el numero cinco. Aqui esta: 5
Veamos que resultado nos da si hacemos 10/5. El resultado es: 2
Una forma tipica de aproximar el numero Pi es haciendo 22/7. El resultado es: 3
En C++ no es lo mismo escribir 22/7 que 22.0/7, el trato es diferente.
Con este simple cambio podemos ver que 22.0/7 es igual a 3.14286
No te parece esto una mejor aproximacion?
宣言 – 呼び出し – 定義 のアプローチをよりよく理解するために、コードの三つの重要な部分に注目してみましょう。
- 5行目: 関数の宣言
void consoladice();は、コード内のどこかにconsoladice()という関数が存在することをコンパイラに伝えます。- 戻り値の型が
voidと指定されており、これは値を返さないことを意味します。 - まだ
consoladice()の実装は知られていませんが、この宣言によってコンパイラは後で使用されるときに認識できるようになります。
- 9行目: 関数の呼び出し
main()関数内でconsoladice();という行が関数を実行します。- この時点で、宣言によって
consoladice()の存在はすでにコンパイラに認識されています。 - 関数が呼び出されると、プログラムの制御はその定義に移り、内容が実行されます。
- 14行目から22行目: 関数の定義
- ここでは
consoladice()の詳細な実装が示され、その命令が記述されています。 - この場合、関数は複数のメッセージをコンソールに出力し、数値や数学的な演算を含みます。
- 重要な点の一つは、
22/7と22.0/7の違いです。22/7の場合、両方の数値は整数であり、整数除算の結果3になります。しかし、22.0/7と書くと、浮動小数点演算が強制され、結果は3.14286となります。
- ここでは
アプローチ:呼び出し前に宣言と実装を行う
C++ では、宣言 – 呼び出し – 定義 のアプローチに加えて、もう一つ有効な方法があります。それが 呼び出し前に宣言と実装を行う という方法です。この方法では、宣言と定義を一度にまとめて行い、関数が main() で使用される前に配置します。
このアプローチでは、関数を事前に宣言して main() の後で定義するのではなく、同じ場所で宣言と定義を行い、その後で呼び出します。これにより、別々に宣言する必要がなくなり、小規模なプログラムではコードがより簡潔で読みやすくなります。
この方法の一般的な構造は次のとおりです。
// main() の前に関数を定義
tipo_de_retorno nombre_de_funcion(lista_de_parametros) {
// 関数本体
return valor; // 必要に応じて
}
int main() {
// 関数の呼び出し
nombre_de_funcion(argumentos);
}
main() の前に定義されているため、コンパイラは呼び出し時点ですでにその関数を認識しており、事前の宣言は不要です。
例: 宣言なしの関数 consoladice()
次に、このアプローチを適用した実際の例を見てみましょう。
#include <iostream>
using namespace std;
// 関数を呼び出す前に定義
void consoladice() {
cout << "Esto es una simple cadena de caracteres o literales." << endl;
cout << "Ahora te muestro el numero cinco. Aqui esta: " << 5 << endl;
cout << "Veamos que resultado nos da si hacemos 10/5. El resultado es: " << 10/5 << endl;
cout << "Una forma tipica de aproximar el numero Pi es haciendo 22/7. El resultado es: " << 22/7 << endl;
cout << "En C++ no es lo mismo escribir 22/7 que 22.0/7, el trato es diferente." << endl;
cout << "Con este simple cambio podemos ver que 22.0/7 es igual a " << 22.0/7 << endl;
cout << "No te parece esto una mejor aproximacion?" << endl;
}
int main() {
// 関数を呼び出す
consoladice();
return 0;
}
このコードから次のことが分かります。
- 5行目から13行目の間で、宣言と定義が統合されている
関数
consoladice()はmain()の前に直接定義されており、別々に宣言する必要はありません。 - 17行目で、関数が
main()内で呼び出されている関数はすでに前に定義されているため、コンパイラはそれを認識し、問題なく実行できます。
- 結果はまったく同じ
機能的には、このアプローチは「宣言 – 呼び出し – 定義」と同じ動作を生成しますが、よりコンパクトな構造になります。
✅ 利点:
- 小規模なプログラムでは、より直接的でコンパクトなコードになる。
- 事前の宣言が不要で、コード行数を減らせる。
- すべての関数が同じファイルにある短いスクリプトでは読みやすさが向上する。
❌ 欠点:
- 大規模なプログラムでは、
main()の前に多くの関数が定義されると整理が難しくなる。 - 複数のファイル(.h や .cpp)で作業する場合にはあまり有効ではなく、宣言を別ファイルにまとめる方が望ましい。
戻り値の伝播
これまでは、単に命令を実行し結果を返さない関数を扱ってきました。しかし、多くの場合、関数が値を返し、その値を他の計算に利用したり、変数に格納したりする必要があります。このプロセスを戻り値の伝播と呼びます。
このセクションでは、値を返す関数がどのように動作するのか、void 型の関数との違い、そして C++ でこの仕組みをどのように活用できるのかを学びます。
実践例: 長方形の面積を計算する関数
戻り値の伝播を説明するために、長方形の底辺と高さを受け取り、その面積を計算する関数を実装してみます。
#include <iostream>
using namespace std;
// 長方形の面積を計算して結果を返す関数
double calcularArea(double base, double altura) {
return base * altura;
}
int main() {
double base, altura;
// ユーザーに値を入力させる
cout << "Ingrese la base del rectangulo: ";
cin >> base;
cout << "Ingrese la altura del rectangulo: ";
cin >> altura;
// 関数を呼び出し、その結果を格納
double area = calcularArea(base, altura);
// 結果を表示
cout << "El Area del rectángulo es: " << area << endl;
return 0;
}
- 関数
calcularArea()が値を返す- 2つの値(
baseとaltura)をパラメータとして受け取ります。 base * alturaによって面積を計算します。returnを使用して、演算結果を呼び出し元のプログラム部分に返します。
- 2つの値(
main()における戻り値の利用baseとalturaの値はユーザーが入力します。- 関数
calcularArea()が呼び出され、その結果は変数areaに格納されます。 - 最終的に、その結果がコンソールに表示されます。
void型関数との重要な違いもし
calcularArea()がvoid型であれば、結果を呼び出し元に返すのではなく、関数内で直接表示しなければなりません。
例: 数が偶数か奇数かを判定する関数
#include <iostream>
using namespace std;
bool esPar(int numero) {
return numero % 2 == 0;
}
int main() {
int numero;
cout << "Ingrese un numero: "; cin >> numero;
if (esPar(numero)) {
cout << "El numero es par." << endl;
} else {
cout << "El numero es impar." << endl;
}
return 0;
}
ここで、関数 esPar() は数が偶数であれば true を返し、奇数であれば false を返します。これにより、main() が結果を利用してどのメッセージを表示するかを決定できます。
再帰: 自分自身を呼び出す関数
再帰とは、関数が自分自身を呼び出して、問題をより小さなバージョンに分割しながら解決する技法です。特に階乗計算、フィボナッチ数列、木構造の探索といったアルゴリズムに有用です。
例: 数の階乗
数 n の階乗は n!=n\cdot(n-1)\cdot(n-2) \cdots 3 \cdot2 \cdot 1 です。この定式化には再帰的な構造があり、次のように数学的に表現できます。
\begin{array}{rl} 0! &=1\\ n! &= n\cdot(n-1)!\\ \end{array}
これを踏まえて、C++ でこの関数を次のようにプログラムできます。
#include <iostream>
using namespace std;
int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}
return n*factorial(n - 1);
}
int main() {
int numero;
cout << "Ingrese un numero: "; cin >> numero;
cout << "El factorial de " << numero << " es " << factorial(numero) << endl;
return 0;
}このコードでは:
- 関数
factorial(n)はn-1を引数にして自分自身を呼び出し、基底条件(n == 0またはn == 1)に到達するまで繰り返します。 - 関数は再帰的に解かれ、値を掛け合わせて最終的な結果を求めます。
例: フィボナッチ数列
フィボナッチ数列は 1, 1, 2, 3, 5, 8, 13, \cdots で表される数列です。この数列は、各項が直前の2つの項の和であるという特徴を持ちます。
数学的には、fibo(n) をフィボナッチ数を返す関数とすると、その構造は次のようになります。
\begin{array}{rl} fibo(0) &= 1\\ fibo(1) &= 1 \\ fibo(n) &= fibo(n-1) + fibo(n-2) \end{array}
次の C++ コードはフィボナッチ数を表示する例です。
#include<iostream>
using namespace std;
int fibo(int numero){
if (numero==0||numero==1){
return 1;
}
return fibo(numero-1)+fibo(numero-2);
}
int main(){
int x=0, i=0;
cout << "ingresa un numero: "; cin >> x;
while (i < x){
cout <<"El numero de fibonacci en la posicion " << i+1 << " es: " << fibo(i)<<endl;
i=i+1;
}
}
関数における複数の戻り値
C++ では、std::pair、std::tuple、あるいは変数への参照を用いることで、1つの関数から複数の値を返すことができます。
例: std::pair を使って2つの値を返す関数
#include <iostream>
#include <utility> // std::pair を使うために必要
using namespace std;
pair<int, int> dividir(int a, int b) {
return make_pair(a / b, a % b);
}
int main() {
int numerador=0, denominador=1;
cout << "ingresa el numerador: "; cin >> numerador;
cout << "Ingresa el denominador: "; cin >> denominador;
pair<int, int> resultado = dividir(numerador, denominador);
cout << "Cociente: " << resultado.first << endl;
cout << "Resto: " << resultado.second << endl;
return 0;
}
ここで、関数 dividir() は2つの値を返しています。それは整数除算の「商」と「余り」です。
例: std::tuple を使って2つ以上の値を返す関数
#include <iostream>
#include <tuple>
using namespace std;
tuple<int, int, int> operaciones(int a, int b) {
return make_tuple(a + b, a - b, a * b);
}
int main() {
int suma=0, resta=0, producto=0;
int a=0, b=0;
cout << "ingresa un numero: "; cin >> a;
cout << "ingresa otro numero: "; cin >> b;
std::tie(suma, resta, producto) = operaciones(a, b);
cout << "Suma: " << suma << ", Resta: " << resta << ", Producto: " << producto << endl;
return 0;
}
関数のオーバーロード (overloading)
関数のオーバーロードを使うと、同じ名前でありながら異なる型や数のパラメータを持つ複数の関数を定義することができます。これにより、コードの可読性と再利用性が向上します。
#include <iostream>
#include <string> // std::string を使うために必要
#include <cmath>
using namespace std;
// 正方形または円(1つの値で表される図形)の面積
double area(double lado) {
return lado * lado;
}
// 長方形(または2つの値で表される図形)の面積
double area(double base, double altura) {
return base * altura;
}
// 三角形(または3つの値で表される図形)の面積
double area(double a, double b, double c){
return 0.25*sqrt((a+b+c)*(a+b-c)*(a-b+c)*(-a+b+c));
}
int main() {
string figura;
double resultado = 0;
double l1=0, l2=0, l3=0;
// 図形を入力
cout << "Que figura es? (cuadrado, circulo, rectangulo o triangulo): ";
cin >> figura;
// if-else で図形を判定
if (figura == "cuadrado") {
cout << "¿Cuanto mide su lado? "; cin >> l1;
resultado = area(l1);
cout << "El area del cuadrado es: " << resultado << endl;
}
else if (figura == "rectangulo"){
cout << "Cuanto mide la base? "; cin >> l1;
cout << "Cuanto mide la altura? ";cin >> l2;
resultado = area(l1, l2);
cout << "El area del rectangulo es: " << resultado << endl;
}
else if (figura == "circulo") {
l1 = 3.141592653;
cout << "Cuánto mide el radio? "; cin >> l2;
resultado = area(l1, l2);
cout << "El area del círculo es: " << resultado << endl;
}
else if (figura == "triangulo"){
cout << "Cuanto miden sus lados?" << endl;
cout << "lado 1: "; cin >> l1;
cout << "lado 2: "; cin >> l2;
cout << "lado 3: "; cin >> l3;
if ((l1+l2+l3)*(l1+l2-l3)*(l1-l2+l3)*(-l1+l2+l3)<0){
cout << "el triangulo es imposible";
}
else {
resultado = area(l1,l2,l3);
cout << "El area del triangulo es: " << resultado << endl;
}
}
else {
cout << "Figura no válida." << endl;
}
return 0;
}C++ におけるインライン関数
C++ の inline 関数は、関数呼び出しのオーバーヘッドを削減することでプログラムのパフォーマンスを最適化する仕組みを提供します。通常の呼び出しを実行する代わりに、コンパイラは関数が呼び出されるたびにそのコードを直接展開しようとします。
インライン関数の構文
inline tipo_de_retorno nombre_de_funcion(lista_de_parametros) {
// 関数本体
return valor; // 必要に応じて
}inline を使用すると、関数を実行するために別のメモリアドレスにジャンプする必要がなくなり、実行時間を短縮できる場合があります。
インライン関数と通常の関数の違い
| 特徴 | 通常の関数 | インライン関数 |
|---|---|---|
| 関数呼び出し | ジャンプによる呼び出しが行われる。 | 使用される場所にコードが直接コピーされる。 |
| 実行時間 | 呼び出しのオーバーヘッドにより遅くなる可能性がある。 | 小さな関数ではより速くなる可能性がある。 |
| メモリ使用量 | メモリに1つの関数コピーのみが保存される。 | 関数が何度も使用されるとバイナリコードのサイズが増える可能性がある。 |
inline 関数の例
#include <iostream>
using namespace std;
inline int cuadrado(int x) {
return x * x;
}
int main() {
cout << "El cuadrado de 5 es: " << cuadrado(5) << endl;
return 0;
}
🔍 コンパイラの処理:
- コンパイラは
cuadrado(5)の呼び出しを直接5 * 5に置き換えます。 - ジャンプによる実行は行われません。
- 計算は関数が呼び出されたのと同じ行で実行されます。
inline の利点と欠点
✅ 利点
- 関数呼び出しのオーバーヘッドを排除: 短くて頻繁に呼び出される関数の実行時間を短縮します。
- コンパイラによる最適化を容易にする: CPU レジスタやスタックの使用を回避することでパフォーマンスを向上させる可能性があります。
- 関数のコードがコンパイル時に利用可能であることを保証します。
❌ 欠点
- バイナリサイズの増加:
inline関数が大規模なプログラムで何度も使用される場合、呼び出しごとにコードが複製されます。これは特に長い関数や頻繁に使われる関数で顕著に現れます。 - 常にインライン展開されるとは限らない: コンパイラが最適でないと判断した場合、
inlineの指示を無視することがあります。
