この章では、関数とプリプロセッサについて説明をしていきます
関数
関数とは、特定のタスクを実行するためのコードのまとまりに名前を付けたものです
関数を使うと、プログラムを分かりやすく再利用可能な部分に分割することができます
関数の定義と呼び出し
関数は以下のように定義します
戻り値の型 関数名(引数名1,引数名2,,){
処理;
return 戻り値;
}戻り値の型
関数が返す値の型を指定します
戻り値が存在しない場合は、voidと記述します関数名
関数を呼び出すときに使う識別子のことを指します引数
関数に渡す値のことで、必要な数だけ定義できます
各引数は型と識別子からなり、カンマで区切ります
引数は関数内で通常の変数として扱われ、関数のローカル変数となります
引数の目的は、関数を呼び出すときに関数に値を渡すことです関数の本体
波括弧 { } で囲まれた関数が実際に行う処理の文の集まりです処理
関数が実行する具体的な処理を記述します戻り値
これは関数の実行を終了させ、戻り値を返すための文です
戻り値の型がvoidである場合は、この文を記述する必要はありません
以下に、戻り値も引数もない関数を定義したプログラムを示します
#include <iostream>
void greet(){
std::cout << "Hello!" << std::endl;
}
int main(){
greet();
return 0;
}void greet(){
greetという関数名の関数を定義します
関数の本体では、標準出力に「Hello!」と出力する処理を記述しています
greet();
greet関数を呼び出しています
greet関数内部での処理が完了すると、greet関数の次の行を実行します
引数と戻り値
引数
関数は引数を使って呼び出し元から処理に必要な値を受け取ることができます
また、一度に複数の値を渡すこともできます
ただし、実引数(関数呼び出しでの引数)の型、個数、並び順は仮引数(関数側の引数)と一致していなければなりません
戻り値
戻り値とは、関数からreturn文を使って呼び出し元に返す値のことをいいます
そして、呼び出し元で戻り値を受け取ることもできます
以下は、2つの整数から加算した値を返すプログラムです
#include <iostream>
int add(int a,int b){
int s = a + b;
return s;
}
int main(){
int x = 10; // 変数xに10を代入
int y = 5; // 変数yに5を代入
int result = add(x, y); // add関数にxとyを渡して、結果をresultに代入
std::cout << "The sum is " << result << std::endl; // 結果を出力
return 0;
}処理の流れは以下のようになります
(1)関数main()の先頭より関数add()までを実行
(2)x,yの値を引数として、関数add()を呼び出す
(3)関数add()内部の処理を実行し、計算結果をreturn文により戻す
(4)関数を抜けて呼び出し元へ戻り、関数add()の戻り値を取得
(5)関数add()以降の処理を実行する
オーバーロード
すでに存在している関数と同じ名前で、「引数の数や型が異なる」関数を定義することをオーバーロードといいます
オーバーロードされた関数は、引数の数や型によって、どの関数が呼び出されるか決定されます
これにより、引数の値や型が異なるものの、似たような処理を行う関数を同じ名前で扱うことができるようになります
オーバーロードのルール
- 引数のリストが異なること
型が異なる: void func(int x) と void func(double x)
数が異なる: void func(int x) と void func(int x, int y) - 戻り値の型はオーバーロードの識別に使われない
int func(int x) と double func(int x) はオーバーロードできません
コンパイラは呼び出し時にどちらの関数を呼び出すべきか判断できないためエラーになります
以下に、サンプルプログラムを示します
#include <iostream>
int sum(int a,int b){
return a+b;
}
int sum(int a,int b,int c){
return a+b+c;
}
double sum(double a,double b){
return a + b;
}
int main(){
int x = sum(1,2);
std::cout << x << std::endl;
int y = sum(1,2,3);
std::cout << y << std::endl;
int z = sum(1.5,2.5);
std::cout << z << std::endl;
return 0;
}オーバーロードの注意点
オーバーロードされた関数の中で、呼び出し時にどの関数が適切かコンパイラが判断できない場合があります
これは、暗黙的な型変換によって複数の関数に適合してしまう場合に起こります
double d = sum(1.5,2);
std::cout << d << std::endl;
このような呼び出しを行った場合、以下のようにコンパイルエラーになります
main12.cpp: In function ‘int main()’:
main12.cpp:30:19: error: call of overloaded ‘sum(double, int)’ is ambiguous
30 | double d = sum(1.5,2);
| ~~~^~~~~~~
main12.cpp:4:5: note: candidate: ‘int sum(int, int)’
4 | int sum(int a,int b){
| ^~~
main12.cpp:13:8: note: candidate: ‘double sum(double, double)’
13 | double sum(double a,double b){
| ^~~
これはintをdoubleへと暗黙の型変換をすることも、doubleからintへと型変換をすることもできるため、コンパイラーがどちらの関数をつかってよいか判断がつかなかったのです
曖昧な呼び出しを避けるためには、明示的なキャストを行うか、より具体的な型のオーバーロードを提供することが必要です
プリプロセッサ
プリプロセッサとは、ソースコードがコンパイルされる前に実行される前処理プログラムのことです
プリプロセッサを使用すると、ソースコード内の指定された文字列や数式が別の文字列や数式に置換されたり、他のファイルの内容がソースコードに取り込まれたり、特定の条件に応じてコンパイルされるコードの範囲を変更したりすることができます
プリプロセッサは、シャープ記号(#)で始まるディレクティブ(指示子)を用いて制御します
プリプロセッサの主な機能を列挙します
- #include:
ヘッダーファイルや別のソースファイルの内容を現在のソースコードに挿入します
例えば、何度も登場している#include <iostream>は、標準入出力に関連する機能が記述されたヘッダーファイル を読み込み、利用できるようにします - #define:
マクロを定義します。マクロは、定数や短いコード片に名前を付けるために使用されます
・定数の定義例
#define PI 3.14この定義により、ソースコード中の PI という文字列はコンパイル前に 3.14 という文字列に置き換えられます
もし 3.14 を 3.14159 に変更したい場合でも、この #define の行を修正するだけで、プログラム全体に反映させることができます
数値がプログラム中に直接記述されている場合に比べて、意味が明確になり、保守性も向上します
・関数のようなマクロの定義例
#define SQUARE(x) ((x) * (x)) このマクロを定義すると、プログラム中で SQUARE(5) と記述された部分は、コンパイル前に ((5) * (5)) に展開されます
マクロSQUAREを呼び出したサンプルプログラムを以下に示します
#include <iostream>
#define PI 3.14 // 円周率を定義する
#define SQUARE(x) ((x) * (x)) // 二乗を計算する数式を定義する
int main() {
double r = 5.0; // 半径
double area = PI * SQUARE(r); // 面積を計算する
std::cout << "半径:" << r << "の面積は" << area << "です" << std::endl;
return 0;
}実行結果
半径:5の面積は78.5です- #if, #ifdef, #ifndef, #else, #elif, #endif
特定の条件に基づいて、コンパイルするコードの範囲を選択する条件付きコンパイルを実現するために使用します
これらのディレクティブは、#if、#ifdef、または#ifndefで始まり、対応する#endifで必ず終了します
以下のプログラムは、DEBUGが定義の有無で出力を変更しているサンプルプログラムです
#include <iostream>
#define PI 3.14 // 円周率を定義する
#define SQUARE(x) ((x) * (x)) // 二乗を計算する数式を定義する
int main() {
double r = 5.0; // 半径
double area = PI * SQUARE(r); // 面積を計算する
#ifdef DEBUG // DEBUGが定義されているとき
std::cout << "r=" << r << std::endl;
#endif
std::cout << "半径:" << r << "の面積は" << area << "です" << std::endl;
#ifndef DEBUG //DEBUGが定義されていないとき
std::cout << "SUCCESS" << std::endl;
#endif
return 0;
}実行結果
半径:5の面積は78.5です
SUCCESSプログラム先頭で
#define DEBUG
と記述した場合の実行結果は以下のようになります
r=5
半径:5の面積は78.5です
マクロ
マクロとは、C++のプリプロセッサによってソースコードの一部を、あらかじめ定義された文字列やコード片で置き換える仕組みのことです
マクロには大きく分けて、オブジェクトのようなマクロと関数のようなマクロの2種類があります
- オブジェクトのようなマクロ
オブジェクトのようなマクロは、主に定数やキーワードなどに名前を付けるために使用されます
これにより、コードの可読性を向上させたり、値を一元的に管理したりすることができます
#define PI 3.14 // 円周率に PI という名前を定義
#define DEBUG // デバッグモードを有効にするためのフラグ- 関数のようなマクロ
関数のようなマクロは、短い処理や式などを関数のように記述するために使用されます
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 2つの値のうち大きい方を返すマクロ
#define PRINT_VALUE(x) std::cout << #x << " = " << x << std::endl // 値とその名前を出力するマクロ以下に、マクロMAXを使用するサンプルプログラムを示します
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 10;
int y = 5;
// マクロを使用する
int max_value = MAX(x, y); // プリプロセッサによって ((x) > (y) ? (x) : (y)) に展開される
std::cout << "The maximum value is: " << max_value << std::endl;マクロ使用時の注意点
マクロは、コンパイル前にプリプロセッサによって単純なテキストの置換として展開されます
そのため、マクロの定義や使用にはいくつかの注意点があります
- 1. 引数の括弧
マクロの引数を使用する際には、必ず括弧で囲むようにしましょう
これにより、予期せぬ演算子の優先順位による問題を回避できます
#define MULTIPLY(a, b) (a * b)
int result = MULTIPLY(2 + 3, 4); // 意図しない結果 (2 + (3 * 4) = 14) になる可能性
int correct_result = MULTIPLY((2 + 3), 4); // 正しい結果 ((5) * 4 = 20)- 2. 副作用のある引数
関数のようなマクロに、インクリメント演算子 (++) やデクリメント演算子 (–) など、評価されるたびに値が変化する副作用のある式を渡すのは避けるべきです
マクロは引数を複数回評価する可能性があるため、予期しない動作を引き起こすことがあります
#define SQUARE(x) ((x) * (x))
int i = 5;
int result = SQUARE(i++); // i が複数回インクリメントされる可能性があり、予期しない結果になる- 3. セミコロン
関数のようなマクロの定義の末尾にセミコロン (;) を書くべきではありません
マクロの呼び出し時にセミコロンを付けるのが一般的です - 4. 可読性とデバッグ
マクロはプリプロセッサによって展開されるため、コンパイラのエラーメッセージは展開後のコードの位置を示すことがあり、デバッグが難しくなることがあります
また、複雑な処理をマクロで記述すると、コードの可読性が低下する可能性があります
これらの注意点を理解し、マクロを適切に使用することで、コードの効率性や可読性を向上させることができます
しかし、複雑な処理や副作用のある操作には、インライン関数などの代替手段を検討することも重要です
可変長引数マクロ
プリプロセッサのマクロ定義において、仮引数の最後に…を記述することで、可変長引数マクロを定義できます
可変長引数は__VA_ARGS__という識別子で参照できます
例えば、次のようなマクロを定義できます
// 可変長引数マクロの例
#define PRINT(…) printf(__VA_ARGS__) // printf関数をラップするマクロ
PRINT(“Hello, world!\n”); // Hello, world!と出力
PRINT(“x = %d, y = %f\n”, x, y); // xとyの値を出力
コメント